---
title: codex()
description: >-
  The Codex harness, its options and id, its OpenAI API key, the tokens and
  served model it records, and the settings that keep it quiet.
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reeval.ai/llms.txt
> Use this file to discover all available pages before exploring further.

`codex()` runs OpenAI's [Codex CLI](https://github.com/openai/codex) in the Docker sandbox, one `codex exec --json` process per turn, on an OpenAI API key. `model` is required.

```ts title="reeval.config.ts"
import { defineConfig } from '{{ pkg }}'
import { codex } from '{{ pkg }}/harness'

export default defineConfig({
  harness: codex({ model: 'gpt-5.6-luna' }),
})
```

## Options

<Property name="model" type="string">
  An OpenAI model id that runs on the Responses API, such as `gpt-5.6-luna`, without a provider. Required: Codex's own default changes between releases.
</Property>

<Property name="effort" type="string" optional>
  Reasoning effort as Codex's `model_reasoning_effort`: `low`, `medium`, `high`, `xhigh`, `max` or `ultra`, depending on the model. The record keeps this requested value next to the served `[effort=…]` suffix. It isn't the measured [effort](/concepts/lift#effort-next-to-lift).
</Property>

<Property name="id" type="string" optional>
  The harness's id in cell keys, filters and the record. Default `codex:<model>`, or `codex:<model>[effort=<value>]` when `effort` is set.
</Property>

<Property name="gateway" type="'direct' | Gateway" optional>
  The gateway this harness's model calls go through, by name or helper, such as `direct()`. It skips the config's `gateway` fallback: the plan refuses a gateway that isn't listed or can't serve this harness. See [Gateways](/reference/gateway#pinning-a-harness).
</Property>

<Property name="maxConcurrent" type="number" optional>
  The most Codex attempts at once across the run, for example for your key's rate limits.
</Property>

<Property name="startTimeout" type="Duration" optional>
  How long its session may take to become ready (bootstrap and session start) before the attempt is an infra error, such as `'3m'`. Default `'90s'`. A start that times out is aborted and started once more; a second timeout fails the attempt with `codex: session did not become ready within 90s (tried twice)`, and the record's `startRetries` notes a retried start.
</Property>

## At a glance

| | |
|---|---|
| Import | `'{{ pkg }}/harness'` |
| Credential | `OPENAI_API_KEY` (an OpenAI API key) |
| Adapter | `@reeval/harness-codex` 0.1.9, bundled with reeval |
| CLI | `@openai/codex` 0.164.0, its native Linux build, checked against npm's sha512 |
| Sandbox | `docker()` only |
| Image | `reeval-sandbox:codex-0.1.9` |
| Rules file | `AGENTS.md` |
| Skills (default placement) | `~/.agents/skills` |
| Skills (`placement: 'workspace'`) | `.agents/skills` in the workspace |
| Egress hosts added | `api.openai.com` |
| Tokens | Reported for each turn, reasoning included |
| Served model | The dated model and reasoning effort, from each API response |
| MCP | `mcp.stdio` and `mcp.http` (no `mcp.sse`) |
| Plugins | `skills` and `mcp`, added with `codex plugin add` (below) |

Build its image once with `reeval image build` in a project whose config uses `codex()`. `gpt-5.6-luna` is in Codex's catalogue and passes the harness contract; pin the model you mean to measure.

## Why not `@ai-sdk/harness-codex`

| | `@ai-sdk/harness-codex` | `@reeval/harness-codex` |
|---|---|---|
| Runs | Codex 0.155.0 behind `@openai/codex-sdk`, in the release that matches reeval's `@ai-sdk/harness` | `codex exec --json` directly, which that SDK wraps |
| Codex's own events | None in its stream, so reeval couldn't check Codex's output format or replay real runs | Every one |
| Reasoning tokens | Dropped | Recorded |

## The key

reeval passes `OPENAI_API_KEY` to the adapter explicitly, and Codex gets a placeholder. The adapter gives Codex its own provider for the OpenAI API, with WebSockets off, so every model call is one HTTP request the gateway can read. The gateway swaps the real key in on that request only:

| Host | Endpoint |
|---|---|
| `api.openai.com` | `POST /v1/responses` |

The adapter refuses to run in a sandbox that can't broker the key.

On a [`gateway` list](/reference/gateway#vercel-ai-gateway) that resolves the harness to `vercel()`, the key is `AI_GATEWAY_API_KEY` (else `VERCEL_OIDC_TOKEN`), Codex's provider is Vercel's Responses API (`https://ai-gateway.vercel.sh/v1`), and the gateway brokers `POST /v1/responses` on `ai-gateway.vercel.sh` instead. A model id without a provider is sent as `openai/<model>`.

On [`openrouter()`](/reference/gateway#openrouter), the key is `OPENROUTER_API_KEY`, Codex's provider is OpenRouter's Responses API (`https://openrouter.ai/api/v1`), and the gateway brokers `POST /api/v1/responses` on `openrouter.ai`. A model id without a provider is sent as `openai/<model>`. Each call records the upstream provider OpenRouter says served it, and its cost; `effort` runs there.

## Tokens and the served model

| `model` | Served model recorded |
|---|---|
| `gpt-5.6-luna` | `gpt-5.6-luna[effort=medium]` |
| `gpt-5.4-mini` | `gpt-5.4-mini-2026-03-17[effort=none]` |

Each turn is one step, with the tokens `turn.completed` reports: input (uncached, and cache reads and writes) and output, with its reasoning tokens.

Codex doesn't say which model served a turn, so the gateway reads it from each response: the Responses API names the model (a dated snapshot for some) and the reasoning effort Codex asked for. The same value is in the attempt's `servedModels` and the run's `resolvedModels`. On `vercel()` it's Vercel's id, such as `openai/gpt-5.6-luna[effort=medium]`.

## Turns and settings

```bash
codex exec --json --skip-git-repo-check \
  --dangerously-bypass-approvals-and-sandbox --model <model>
```

Each turn runs that, and later turns add `resume <thread>`, so they continue the conversation. The Docker sandbox is the isolation, so Codex's own sandbox and approvals are off. The prompt is a command-line argument, so it must stay under 128 KiB.

Instructions go to `~/.codex/AGENTS.md`, which Codex reads before the workspace's. Codex takes each model's tools and default reasoning effort from the catalogue bundled in its release (`codex debug models --bundled` lists it).

A model the catalogue doesn't list, such as `gpt-5.4-mini` since Codex 0.158.0, runs on fallback settings: no `apply_patch` tool and no reasoning effort. The agent then edits files from the shell, and the served model records `[effort=none]`.

The adapter writes `~/.codex/config.toml` with Codex's defaults but for what it sends besides model requests:

| Setting | Stops |
|---|---|
| `analytics.enabled = false` | Analytics to `ab.chatgpt.com` |
| `features.plugins = false` | The plugin sync from `github.com` and `chatgpt.com`, in a session without [plugins](#plugins) |

With them, a turn contacts only `api.openai.com`, so `egress: 'deny-all'` needs nothing else. Codex's web search and the pages it opens run on OpenAI's side: a restricted policy doesn't limit them, and each is a `web-search` or `web-fetch` event in the [transcript](/reference/transcript).

## MCP

| Server | In `config.toml` under `mcp_servers` |
|---|---|
| `mcp.stdio` | `command`, `args` and `env` |
| `mcp.http` | `url` and `http_headers` |
| `mcp.sse` | Refused at plan time, naming the server: Codex has no SSE transport |

## Plugins

```json title="~/.agents/plugins/marketplace.json"
{
  "name": "reeval",
  "plugins": [
    {
      "name": "review",
      "source": { "source": "local", "path": "./.reeval/plugins/review" }
    }
  ]
}
```

reeval copies each plugin to `~/.reeval/plugins/<name>/` and lists it in Codex's home marketplace. Before the first turn the adapter runs `codex plugin add review@reeval`, offline, which copies the plugin into `~/.codex/plugins/cache/reeval/` and enables it in `config.toml`. Codex loads each plugin namespaced, so its skills don't clash with yours.

A session with plugins sets `features.plugins = true`, and Codex then syncs its plugin catalogue on every turn. Under a restricted policy, its calls to `github.com`, `api.github.com` and `chatgpt.com` are refused and recorded under `egress.expectedBlocked`. In a session without plugins, a refused call to those hosts is the agent's own, so it stays in `egress.blocked`.

Codex starts a plugin's MCP servers itself, so reeval leaves them out of `mcp_servers`. It names them by their own keys, so the transcript names a call `mcp__docs__<tool>`, and the plan refuses a plugin server named like one in the environment's `mcp`, which Codex would drop. As for [Claude Code](/reference/harness/claude-code#plugins), the placed `.mcp.json` runs each through reeval's MCP timer, and a plugin server that reads a host secret is refused at plan time; declare that server in `mcp`. The plan refuses any part Codex doesn't load:

```text
reeval: evals/p.eval.ts: evaluate("reviews") on codex:gpt-5.4-mini: Codex can't load the review plugin's hooks (hooks/hooks.json): use only: ['skills'], or leave the plugin out of this cell
reeval: evals/p.eval.ts: evaluate("reviews") on codex:gpt-5.4-mini: Codex can't load the chat plugin's apps: they need a ChatGPT login
```

A plugin has apps when its manifest names `apps` or it has an `.app.json`. Codex reads a plugin's `.codex-plugin/plugin.json`, or a root `plugin.json` with an Agent Plugins `$schema`, in place of its `.claude-plugin/plugin.json`, so the plan refuses a plugin that has either: reeval couldn't hold Codex to the parts it places.

## Errors

| Situation | Message |
|---|---|
| No model, or one with a provider | `codex() needs an OpenAI model id, without a provider, since it runs on an OpenAI API key, e.g. codex({ model: 'gpt-5.6-luna' }) (got …)` |
| No key when the attempt starts | `OPENAI_API_KEY isn't set, so the Codex harness can't authenticate. Set it in the environment or .env.local.` |
| OpenAI refuses the key | `The API rejected OPENAI_API_KEY: set a valid key in the environment or .env.local. unexpected status 401 Unauthorized: …` |
| `mcp.sse` (at plan time, exit 2) | `evaluate("<eval>") on codex:<model>: Codex has no SSE MCP transport, so it can't use the <name> server (mcp.sse): use mcp.http or mcp.stdio` |
| A non-Docker sandbox | `The Codex harness needs the Docker sandbox: set sandbox: docker() …` |
| Codex dies mid-turn | `codex exited with code <n> before ending the turn: …` |

A refused key is retried five times by Codex before the turn fails; each retry is a `retry` event in the transcript.

## Related

- [Harnesses](/guide/harnesses)
- [Secrets and egress](/guide/secrets-and-egress)
- [Transcripts](/reference/transcript)
