This guide covers how to use EvalBench for evaluating OpenAI Codex CLI (codex exec) agent workflows using MCP Servers (HTTP/streamable and stdio). It includes configuration reference, evaluation dataset format, scoring metrics, and step-by-step instructions for running evaluations locally.
The Codex CLI generator mirrors the Gemini CLI and Claude Code generators in this repo — same evalset format, same orchestrator, same scorers — so most of what you know about Gemini CLI evaluation and Claude Code evaluation carries over.
- Overview
- Architecture
- Prerequisites
- Quick Start
- Configuration Reference
- Authentication
- MCP Servers
- Sandbox & Approval Modes
- Pricing & Cost Tracking
- Scorers
- End-to-End Examples
- Troubleshooting
EvalBench's Codex CLI integration enables automated, multi-turn evaluation of agentic AI workflows powered by OpenAI's Codex CLI. The CLI acts as the orchestrator that connects to MCP server backends and executes scenarios defined in an evaluation dataset. A simulated user powered by an LLM drives multi-turn conversations following a conversation plan.
- Multi-turn evaluation with LLM-powered simulated users (uses
codex exec resume <session_id>to continue prior threads) - API-key auth sourced from
OPENAI_API_KEYenv or Google Secret Manager - Two MCP transport modes: streamable HTTP (with Google Cloud OAuth auto-injection) and stdio
- Pinned CLI versions via
npm exec(matches Gemini CLI / Claude Code) - Cost tracking via configurable per-model pricing (Codex NDJSON ships tokens but not USD)
- 8 built-in scorers covering correctness, efficiency, and behavior quality
- CSV and BigQuery reporting
| Aspect | Same / Different |
|---|---|
| Evalset JSON format | Same — scenarios[] with id, starting_prompt, conversation_plan, expected_trajectory, max_turns, env |
dataset_format |
agent-format |
orchestrator |
agent |
| Scorers | Same (trajectory_matcher, goal_completion, behavioral_metrics, etc.) |
| Simulated user | Same (simulated_user_model_config) |
| Reporting | Same (CSV / BigQuery) |
| MCP server config | Same Gemini-style schema (httpUrl, authProviderType: google_credentials, headers) — auto-translated to Codex's TOML format |
| Skills & Extensions | Same — supports installing skills from git repos or local directories in the sandboxed environment |
┌─────────────────────────────────────────────────────────────────────┐
│ EvalBench Pipeline │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ │
│ │ Run Config │───▶│ AgentOrchestrator│───▶│ AgentEvaluator │ │
│ │ (YAML) │ │ │ │ │ │
│ └──────────────┘ └──────────────────┘ └────────┬──────────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────────────┼──────────┐ │
│ │ Eval Dataset│ │ Per Scenario │ │ │
│ │ (JSON) │─────────────▶│ ▼ │ │
│ └──────────────┘ │ ┌──────────────────────────┐ │ │
│ │ │ CodexCliGenerator │ │ │
│ ┌──────────────┐ │ │ ┌──────────┐ ┌────────┐ │ │ │
│ │ Model Config │──────────────│─▶│ │ MCP / │ │Sim. │ │ │ │
│ │ (YAML) │ │ │ │ codex CLI│ │User │ │ │ │
│ └──────────────┘ │ │ └──────────┘ └────────┘ │ │ │
│ │ └───────────┬──────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Scorers (8 metrics) │ │ │
│ │ └──────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Reporting (CSV / BigQuery) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Flow:
- The Run Config ties together the dataset, model config, scorers, and reporting.
- The AgentOrchestrator (
orchestrator: agent) loads theAgentEvaluator. - The evaluator instantiates CodexCliGenerator based on
generator: codex_cliin the model config. - On startup, the generator writes a sandboxed
~/.codex/config.toml(with translated MCP servers) and~/.codex/auth.json(with the API key) into.venv/fake_home_codex/so the host machine's~/.codexis not touched. - For each scenario, the evaluator runs a multi-turn loop:
- Sends the starting prompt to Codex via
codex exec --json --skip-git-repo-check ... <prompt> - A SimulatedUser (LLM) generates realistic follow-up responses
- Subsequent turns use
codex exec resume <session_id>to continue the same Codex thread - Tools and stats are accumulated across turns from the NDJSON ThreadEvent stream
- Conversation continues until
max_turnsis reached or the simulated user sendsTERMINATE
- Sends the starting prompt to Codex via
- Results are scored and written to CSV and/or BigQuery.
- Python 3.10+ and project dependencies installed
- Node.js and npm (for running Codex CLI via
npm exec) - Codex CLI — either:
- Globally installed:
npm install -g @openai/codex(then usecodex_cli_version: "codex"), or - Pinned version (recommended for reproducibility):
codex_cli_version: "@openai/codex@latest"(or a specific version like"@openai/codex@0.30.0") —npm exec --yeswill install it on first use
- Globally installed:
- OpenAI API key — see Authentication. The key must be supplied in the model config (or env). Codex's ChatGPT-OAuth fallback is not usable from CI — accounts without ChatGPT-Plus get
402 deactivated_workspace. - Environment variables for the simulated user / scorer model:
export EVAL_GCP_PROJECT_ID=your_project_id export EVAL_GCP_PROJECT_REGION=us-central1
- gcloud (only if any MCP server uses
authProviderType: google_credentials— the generator shells out togcloud auth application-default print-access-token; rungcloud auth application-default loginfirst)
# Real MCP server (Cloud SQL Admin API):
export EVAL_CONFIG=datasets/codex-cli-tools/example_run_config.yaml
# Fake MCP (offline testing, deterministic):
export EVAL_CONFIG=datasets/codex-cli-tools/example_run_fake_config.yaml./evalbench/run.shResults land in results/<job_id>/ as CSV files.
The top-level config that ties everything together. Identical to the other agent run configs.
| Key | Required | Description |
|---|---|---|
dataset_config |
Yes | Path to the evalset JSON file |
dataset_format |
Yes | agent-format |
orchestrator |
Yes | agent |
model_config |
Yes | Path to the Codex CLI model config YAML |
simulated_user_model_config |
Yes | Path to the model config for the simulated user LLM |
scorers |
Yes | Dictionary of scorer configurations |
runners.agent_runners |
Optional | Concurrency (default 10). Set to 1 for sequential runs — recommended for Codex because all scenarios share the same sandboxed ~/.codex store. |
reporting |
Optional | CSV and/or BigQuery output options |
Example (example_run_config.yaml):
dataset_config: datasets/codex-cli-tools/codex-cli.evalset.json
dataset_format: agent-format
orchestrator: agent
model_config: datasets/model_configs/codex_cli_model.yaml
simulated_user_model_config: datasets/model_configs/gemini_2.5_pro_model.yaml
# Run scenarios sequentially. Codex shares one ~/.codex/config.toml across
# the runner pool; serial runs avoid session-id collisions on `codex exec resume`.
runners:
agent_runners: 1
scorers:
trajectory_matcher: {}
goal_completion:
model_config: datasets/model_configs/gemini_2.5_pro_model.yaml
behavioral_metrics:
model_config: datasets/model_configs/gemini_2.5_pro_model.yaml
parameter_analysis:
model_config: datasets/model_configs/gemini_2.5_pro_model.yaml
turn_count: {}
end_to_end_latency: {}
tool_call_latency: {}
token_consumption: {}
reporting:
csv:
output_directory: 'results'The model config defines the Codex CLI version, model, auth, sandbox/approval policy, environment, MCP server setup, and pricing.
| Key | Required | Description |
|---|---|---|
codex_cli_version |
Yes | Either "codex" (uses the globally installed binary) or an npm spec like "@openai/codex@latest" / "@openai/codex@0.30.0" (uses npm exec --yes) |
generator |
Yes | Must be codex_cli |
model |
Yes | Model id passed to codex exec -m <model> (e.g., "gpt-5.5", "o4-mini", "gpt-4.1") |
openai_api_key_secret |
Optional | Google Secret Manager resource path for the API key. Bare form projects/.../secrets/.../versions/<N> or secret_manager://projects/.../secrets/.../versions/<N> URL form. Numeric version required — latest not supported. |
env.OPENAI_API_KEY |
Optional | Direct API key. Also accepts a Secret Manager path here (auto-detected and resolved). Prefer openai_api_key_secret or shell env over hardcoding. |
sandbox_mode |
Optional | "danger-full-access" (default — passes --dangerously-bypass-approvals-and-sandbox), or any value Codex's --sandbox flag accepts (e.g., "workspace-write", "read-only") |
approval_mode |
Optional | Forwarded to --ask-for-approval when sandbox_mode != "danger-full-access". Default "never". |
profile |
Optional | Codex profile name (forwarded as --profile <name>) |
json_flag |
Optional | "--json" (default, newer Codex versions) or "--experimental-json" (older versions). Codex requires NDJSON for the eval pipeline to extract tool calls and tokens. |
pricing |
Optional | Per-model rates used to compute cost_usd per turn. See Pricing & Cost Tracking. |
env |
Optional | Environment variables passed to the CLI process (e.g., GOOGLE_CLOUD_PROJECT for Cloud SQL MCP) |
setup.mcp_servers |
Optional | MCP server configurations (see MCP Servers) |
setup.config |
Optional | Free-form key/value pairs written to the top of ~/.codex/config.toml. Merged on top of the default forced_login_method = "api". |
Example (codex_cli_model.yaml):
codex_cli_version: "@openai/codex@latest"
generator: codex_cli
model: "gpt-5.5"
openai_api_key_secret: <secret>
pricing:
input_per_million_usd: 1.25
cached_input_per_million_usd: 0.125
output_per_million_usd: 10.0
env:
GOOGLE_CLOUD_PROJECT: "astana-evaluation"
GOOGLE_CLOUD_LOCATION: "us-central1"
setup:
mcp_servers:
"cloud-sql":
httpUrl: "https://sqladmin.googleapis.com/mcp"
authProviderType: google_credentials
headers:
X-Goog-User-Project: astana-evaluationUses the shared scenario schema. See the agentic dataset format for the full field reference, including the canonical tool name format used in expected_trajectory.
Minimal example (codex-cli.evalset.json):
{
"scenarios": [
{
"id": "cloud-sql-list-instances-01",
"starting_prompt": "list all Cloud SQL instances in project astana-evaluation",
"conversation_plan": "Ask the agent to list instances in project astana-evaluation. Once all instances are listed if nl2code exists get its state and validate it is RUNNABLE.",
"expected_trajectory": ["cloud-sql__list_instances", "cloud-sql__get_instance"],
"env": { "GOOGLE_CLOUD_PROJECT": "astana-evaluation" },
"kind": "tools",
"max_turns": 3
}
]
}Codex CLI uses an OpenAI API key. The generator resolves it in this order, then writes it to ~/.codex/auth.json (the same file codex login --api-key produces) so codex exec picks it up:
openai_api_key_secretin the model config — a Google Secret Manager resource path. Bare formprojects/<num>/secrets/<NAME>/versions/<N>orsecret_manager://projects/...URL form. Requires a numeric version (nolatest).env.OPENAI_API_KEYin the model config, orOPENAI_API_KEYin the shell env. If the value itself looks like a Secret Manager path, it's resolved transparently.
# Option 1: Secret Manager (recommended for shared / CI environments)
openai_api_key_secret: <secret>
# Option 2: shell env (recommended for local dev)
# export OPENAI_API_KEY=sk-...
env: {}
# Option 3: inline (avoid — gets committed)
env:
OPENAI_API_KEY: "sk-..."The generator writes <fake_home>/.codex/auth.json with {"auth_mode": "apikey", "OPENAI_API_KEY": "<key>"} and chmods it to 0600. It also sets forced_login_method = "api" in config.toml so Codex never tries the ChatGPT-OAuth flow.
Why not just rely on the env var? Codex's auth manager only honors
OPENAI_API_KEYwhen an internalenable_codex_api_key_envflag is set. Forcodex execthe canonical path isauth.json, so the generator writes both.
EvalBench accepts the same MCP server config schema as Gemini CLI and Claude Code. The Codex generator auto-translates it into Codex's TOML schema at runtime (see _translate_mcp_config):
| Gemini-style field | Codex translation |
|---|---|
httpUrl |
→ url (TOML, streamable HTTP server) |
headers |
→ http_headers (TOML inline table) |
authProviderType: google_credentials |
→ mints an ADC token (gcloud auth application-default print-access-token, falling back to gcloud auth print-access-token) and passes it to Codex via bearer_token_env_var (env var EVALBENCH_GCLOUD_MCP_TOKEN). The generator re-mints a fresh token before every turn (each codex exec re-reads the env var), so it doesn't expire mid-suite. X-Goog-User-Project stays a static http_headers entry. Google API MCP endpoints reject the plain user token on tool calls — see Troubleshooting. |
oauth.scopes |
(dropped — Codex doesn't use Gemini's OAuth delegation) |
command / args / env / cwd (stdio) |
→ passed through as-is into a [mcp_servers.NAME] stdio block |
setup:
mcp_servers:
"cloud-sql":
httpUrl: "https://sqladmin.googleapis.com/mcp"
authProviderType: google_credentials
headers:
X-Goog-User-Project: astana-evaluationThis generates the following block in ~/.codex/config.toml:
forced_login_method = "api"
[mcp_servers.cloud-sql]
url = "https://sqladmin.googleapis.com/mcp"
http_headers = { "X-Goog-User-Project" = "astana-evaluation", "Authorization" = "Bearer ya29...." }setup:
mcp_servers:
"cloud-sql":
command: "python"
args:
- "evalbench/util/fake_mcp_server.py"
- "--server-name"
- "cloud-sql"
- "--config"
- "datasets/model_configs/codex_cli_fake_model.yaml"→ TOML:
[mcp_servers.cloud-sql]
command = "python"
args = ["evalbench/util/fake_mcp_server.py", "--server-name", "cloud-sql", "--config", "datasets/model_configs/codex_cli_fake_model.yaml"]CodexCliGenerator._setupwrites the translated config to<fake_home>/.codex/config.toml_write_codex_auth_jsonwrites the API key to<fake_home>/.codex/auth.json- The CLI is invoked with
HOME=<fake_home>so it loads only the configured servers (no host-machine pollution) - Each scenario runs in a sandboxed
HOME(.venv/fake_home_codex/locally,/tmp_sessions/<session_id>/fake_homein gRPC mode)
Codex CLI evaluations support Skills and Extensions (plugins). These are installed during the setup phase into the sandboxed ~/.codex directory.
You can specify skills to install in the setup section of your model configuration:
setup:
skills:
- action: install_from_repo
url: "https://github.com/gemini-cli-extensions/cloud-sql-postgresql.git"The generator will:
- Clone the repository into
<fake_home>/.codex/plugins/ - Register the plugin in
<fake_home>/.codex/plugins/marketplace.json - Install the individual skills into
<fake_home>/.codex/skills/
These skills are then available for the Codex agent to use during the evaluation turns.
Codex CLI sandboxes file/network access by default and prompts for approval before tool calls. For automated evaluation, the generator disables both by default — equivalent to Gemini CLI's --yolo and Claude Code's --dangerously-skip-permissions.
sandbox_mode value |
Behavior | Resulting flags |
|---|---|---|
"danger-full-access" (default) |
Fully bypass sandbox + approvals | --dangerously-bypass-approvals-and-sandbox |
"workspace-write" |
Allow writes only inside the working tree | --sandbox workspace-write --ask-for-approval <approval_mode> |
"read-only" |
No writes anywhere | --sandbox read-only --ask-for-approval <approval_mode> |
approval_mode (default "never") is forwarded as --ask-for-approval <mode> whenever sandbox_mode is not danger-full-access. Use "never" for unattended runs; any prompt-driven mode will hang the evaluator.
# Default (most permissive — recommended for CI)
sandbox_mode: "danger-full-access"
# Stricter — block writes outside repo, never prompt
sandbox_mode: "workspace-write"
approval_mode: "never"Codex's NDJSON includes usage.input_tokens, usage.output_tokens, and usage.cached_input_tokens per turn but does not include cost. Provide a pricing block in the model config and the generator will compute cost_usd per turn for the token_consumption scorer.
Two equivalent forms are accepted; pick whichever is easier to copy from OpenAI's pricing page.
Per-million form (matches the OpenAI pricing page):
pricing:
input_per_million_usd: 1.25
cached_input_per_million_usd: 0.125 # optional; defaults to 10% of input
output_per_million_usd: 10.0Per-token form:
pricing:
input_per_token_usd: 0.00000125
cached_input_per_token_usd: 0.000000125
output_per_token_usd: 0.00001Cost formula:
billable_input = max(0, input_tokens - cached_input_tokens)
cost_usd = (billable_input * input_rate)
+ (cached_input_tokens * cached_input_rate)
+ (output_tokens * output_rate)
If pricing is missing or malformed, cost_usd is reported as 0.0 (rather than guessing). Update the rates whenever you change model: — pricing differs by model.
See the scorer reference for the full catalog and configuration options.
Quick reference:
| Scorer | Type | Description |
|---|---|---|
trajectory_matcher |
Deterministic | Jaccard or Levenshtein match between expected and actual tool trajectory. Native Codex tools (shell, file ops, ...) are dropped from both sides by default — set filter_native_tools: false to score them too. |
goal_completion |
LLM | Did the agent accomplish the conversation plan? |
behavioral_metrics |
LLM | Hallucination rate + clarification rate |
parameter_analysis |
LLM | Qualitative feedback on tool parameters |
turn_count |
Deterministic | Number of conversation turns |
end_to_end_latency |
Deterministic | Total wall-clock latency of the codex exec subprocess |
tool_call_latency |
Deterministic | Sum of per-tool durations measured between item.started and item.completed arrival times (Codex events carry no timestamps, so the generator stamps arrival in-process) |
token_consumption |
Deterministic | Total input + output + cached tokens, plus cost_usd from the pricing block |
The Codex generator extracts tool calls from the following NDJSON ThreadItem kinds: mcp_tool_call, command_execution (reported as shell), web_search, and file_change.
Goal: Use Codex to manage Cloud SQL instances via the public Cloud SQL Admin MCP.
# datasets/model_configs/codex_cli_model.yaml
codex_cli_version: "@openai/codex@latest"
generator: codex_cli
model: "gpt-5.5"
openai_api_key_secret: "projects/393137573/secrets/OPENAI_API_KEY/versions/1"
pricing:
input_per_million_usd: 1.25
cached_input_per_million_usd: 0.125
output_per_million_usd: 10.0
env:
GOOGLE_CLOUD_PROJECT: "astana-evaluation"
setup:
mcp_servers:
"cloud-sql":
httpUrl: "https://sqladmin.googleapis.com/mcp"
authProviderType: google_credentials
headers:
X-Goog-User-Project: astana-evaluationRun:
gcloud auth login # for the OpenAI Secret Manager fetch
gcloud auth application-default login # for the Cloud SQL MCP token
export EVAL_GCP_PROJECT_ID=astana-evaluation
export EVAL_CONFIG=datasets/codex-cli-tools/example_run_config.yaml
./evalbench/run.shUse the bundled fake MCP server when you want to exercise the agent without any network calls — useful in CI or when iterating on a scenario file.
export EVAL_CONFIG=datasets/codex-cli-tools/example_run_fake_config.yaml
./evalbench/run.shThis config (example_run_fake_config.yaml) launches evalbench/util/fake_mcp_server.py over stdio with the canned tool responses defined under fake_mcp_tools in codex_cli_fake_model.yaml.
Skip Secret Manager entirely:
codex_cli_version: "@openai/codex@latest"
generator: codex_cli
model: "gpt-5.5"
# no openai_api_key_secret — OPENAI_API_KEY picked up from shell env
setup:
mcp_servers:
"cloud-sql":
httpUrl: "https://sqladmin.googleapis.com/mcp"
authProviderType: google_credentials
headers:
X-Goog-User-Project: astana-evaluationexport OPENAI_API_KEY=sk-...
export EVAL_CONFIG=datasets/codex-cli-tools/example_run_config.yaml
./evalbench/run.shCodex fell back to ChatGPT-OAuth because no API key was found. Check:
openai_api_key_secretresolves successfully (look forFailed to fetch OPENAI_API_KEY from Secret Managerin logs)- Or
OPENAI_API_KEYis set in your shell / model configenv - The generator logs
Codex API key resolved (length=N) and written to .../auth.jsonon a successful resolve. If you seeCodex API key could not be resolved, fix that first.
Your installed Codex predates the --json flag. Either upgrade (codex_cli_version: "@openai/codex@latest") or fall back to the experimental flag:
json_flag: "--experimental-json"- For pinned versions (
@openai/codex@...): make surenodeandnpmare onPATH.npm exec --yeswill download Codex on first use. - For
codex_cli_version: "codex": install globally withnpm install -g @openai/codex.
The injected token is missing, expired, the wrong kind, or your principal lacks IAM access. Note Google API MCP endpoints leave initialize/tools/list unauthenticated but require a valid bearer token on the actual tool call, and they only accept an ADC token — the plain gcloud user token (gcloud auth print-access-token) is rejected. The generator now injects the ADC token (gcloud auth application-default print-access-token); make sure:
- You ran
gcloud auth application-default login(with--scopes=https://www.googleapis.com/auth/cloud-platform) — not justgcloud auth login. - Your account / service account has the required IAM roles (e.g.,
roles/cloudsql.admin). X-Goog-User-Projectheader points at a project that has the Cloud SQL Admin API enabled.- Token expiry is handled automatically: the generator mints a fresh ADC token into
EVALBENCH_GCLOUD_MCP_TOKENbefore every turn and Codex reads it viabearer_token_env_var, so long suites don't hit stale-token 401s. (Requires a Codex build that supportsbearer_token_env_var; if yours doesn't, the token won't be applied — fall back to a statichttp_headersAuthorization.)
Either a manual edit to ~/.codex/config.toml clobbered the generated file, or a hand-written setup.config value contains an unsupported type. Fix:
rm -rf .venv/fake_home_codex…and re-run. The generator regenerates config.toml on every invocation.
These show up as [error] ... in the response field when codex exec resume is called against a thread that Codex couldn't write to. The matching failed to record rollout items: thread <id> not found lines on stderr are scrubbed by the generator (see _STDERR_NOISE_PATTERNS). Causes:
- Two scenarios racing on the same fake
HOME— setrunners.agent_runners: 1. - A previous run left a corrupt
~/.codex/sessions/—rm -rf .venv/fake_home_codex/.codex/sessionsand re-run.
The simulated user failed to initialize. Check EVAL_GCP_PROJECT_ID is set if your simulated user model uses Vertex AI:
export EVAL_GCP_PROJECT_ID=astana-evaluationEither no pricing block was provided, or it's malformed. Check the model-config YAML and look for Codex pricing config missing input/output rates; cost_usd will be 0. in the logs. Pricing keys must be strict <thing>_per_million_usd or <thing>_per_token_usd — typos silently fall through.
This is expected when agent_runners > 1. Codex shares one ~/.codex/config.toml and one auth.json across the runner pool, so concurrent scenarios are safe but their NDJSON streams will interleave in the eval logs. To force sequential execution:
runners:
agent_runners: 1npm exec --yes @openai/codex@<version> downloads the package on first use (~30–60 sec). Subsequent runs hit the npm cache.
If a previous run left bad state:
rm -rf .venv/fake_home_codexThe generator recreates ~/.codex/config.toml and ~/.codex/auth.json on the next invocation.
- Gemini CLI Evaluation Guide — sister doc, shares most concepts
- Claude Code Evaluation Guide — sister doc, shares most concepts
- Codex CLI source / docs — official CLI reference
- Model Context Protocol (MCP) — protocol used by tool servers
- CodexCliGenerator implementation