| description | Run OpenAI Codex as the agent under evaluation in Coder Eval — installation, authentication, task configuration, and how Codex telemetry maps to sandboxed, weighted scoring. |
|---|
Coder Eval can run OpenAI's Codex as the agent under evaluation, via the official Codex SDK. The CodexAgent mirrors the structure of ClaudeCodeAgent and plugs into the same sandbox, scoring, and telemetry pipeline — set agent.type: codex in a task and the rest of the framework works unchanged.
Install coder-eval with the codex extra:
pip install 'coder-eval[codex]'This installs:
openai-codex- The official Codex Python SDK (from PyPI)openai-codex-cli-bin- Platform-specific Codex CLI binaries (pulled in transitively)
Codex requires authentication. Options:
# Option 1: API Key (direct)
await codex_client.login_api_key("your-api-key")
# Option 2: ChatGPT (interactive)
await codex_client.login_chatgpt()
# Option 3: Device Code Flow
await codex_client.login_chatgpt_device_code()CodexAgent.start() calls login_api_key automatically when CODEX_API_KEY is present in the environment. Without a key it falls back to any existing ChatGPT login. (Only CODEX_API_KEY is read — not OPENAI_API_KEY/AZURE_OPENAI_API_KEY; point CODEX_API_KEY at whichever endpoint's key you use.)
| Env var | Purpose |
|---|---|
CODEX_API_KEY |
Auth key/token for the selected endpoint (required for headless runs). |
CODEX_BASE_URL |
Route to a custom endpoint. Unset → standard OpenAI platform (api.openai.com). Set → custom provider (gateway or Azure). |
CODEX_MODEL |
Fallback model when agent.model is unset. On Azure this is the deployment name. |
CODEX_API_VERSION |
Azure only: the required api-version query param. Leave unset for OpenAI/gateways. |
Standard OpenAI: leave CODEX_BASE_URL unset, set CODEX_API_KEY to an OpenAI sk-… key and CODEX_MODEL to a Codex/Responses-capable model.
Azure OpenAI:
CODEX_BASE_URL=https://<your-resource>.openai.azure.com/openai
CODEX_API_VERSION=2025-04-01-preview # required by Azure
CODEX_MODEL=<your-deployment-name> # deployment, not the base model id
CODEX_API_KEY=<azure-openai-key>This registers a custom Codex model provider (base_url + env_key=CODEX_API_KEY + query_params={api-version} + wire_api=responses). The Codex CLI only supports the Responses wire API (it rejects wire_api=chat as "no longer supported"), so the protocol is fixed. If your Azure deployment requires the key in an api-key header rather than Authorization: Bearer, that needs an additional provider http_headers/env_http_headers entry — open an issue if you hit that.
Run a task with Codex agent:
coder-eval run tasks/agents/codex_hello_world.yaml --type codexOr override agent type for all tasks in an experiment:
coder-eval run experiments/model-comparison.yaml --type codexSpecify Codex in task YAML:
agent:
type: codex
permission_mode: acceptEdits
allowed_tools:
- Bash
- Read
- Write
disallowed_tools:
- Edit
plugins:
- type: local
path: "$PLUGIN_PATH"
success_criteria:
- type: file_exists
path: "src/solution.py"
description: "Solution file must exist"Valid permission_mode values:
default- Standard access, requires approval on failureacceptEdits- Automatically accept file edits, no filesystem restrictionsplan- Read-only sandbox, approval required for any changesbypassPermissions- Full access, no approvals needed
CodexAgent supports SKILL.md files following the Agent Skills open standard. Skills are discovered from:
- config.plugins - Local plugins with
type: localandpathpointing to a skills directory - plugin_tools_dir parameter - Runtime plugin directory passed to
start()
Skills are symlinked (or copied) to .agents/skills/ where the Codex CLI auto-discovers them. Environment variables in plugin paths ($VAR, ${VAR}) are expanded at runtime.
Example with environment variable:
agent:
type: codex
plugins:
- type: local
path: "$SKILLS_PLUGIN_PATH"Set environment variable:
export SKILLS_PLUGIN_PATH=~/uipath/uipath-claude-plugins/plugins/uipath-coded-agents
coder-eval run tasks/my_task.yamlAgent (ABC)
└── CodexAgent
├── Codex SDK Client (openai_codex.Codex)
├── Thread Management (thread_start, turn.stream)
└── Streaming telemetry (commands, token usage, agent text)
start(working_directory)- Initialize Codex client and set working directorycommunicate(user_input, timeout, stream_callback)- Execute one turn with Codexstop()- Clean up resourcesget_state()- Return current agent statediscard_pending_turn()- Rollback on failure
Each turn returns a TurnRecord with:
iteration- Turn numberuser_input- The prompt sentagent_output- assembled from the streamedagentMessagedeltascommands-CommandTelemetryfor each shell command (Bash) and apply_patch file change (Write)timestamp- When the turn completedduration_seconds- Wall-clock execution timetoken_usage- input/output/cache-read token counts (from the SDK token-usage stream)model_used- the pinnedagent.model, when set
The agent uses a ThreadedWatchdog to enforce wall-clock timeouts. If a turn exceeds the deadline, a TurnTimeoutError is raised with a partial TurnRecord preserved in pending_turn.
On failure, the agent:
- Sets
pending_turnto acrashed=TrueTurnRecord with captured telemetry - Raises
AgentCrashErrororTurnTimeoutError - The orchestrator reads
pending_turnand callsdiscard_pending_turn()to roll back state
The agent maps permission_mode to the Codex SDK's Sandbox. The approval mode is uniformly deny_all for every mode — the trust boundary is the sandbox, which does vary by mode:
permission_mode |
sandbox |
approval_mode |
|---|---|---|
bypassPermissions |
full-access |
deny_all |
acceptEdits |
workspace-write |
deny_all |
default |
workspace-write |
deny_all |
plan |
read-only |
deny_all |
deny_all means run autonomously, never prompt, no server-side reviewer: in-sandbox operations execute directly and only escalations beyond the sandbox are refused. Coder Eval uses it for every mode because the alternative (auto_review) adds a server-side reviewer that can spuriously return declined under gateway load.
allowed_tools / disallowed_tools are normalized (Bash → shell, Write/Edit → apply_patch, etc.) and passed as enabled_tools / disabled_tools in the thread config. Note: the Codex SDK does not currently enforce disabled_tools; do not rely on it as a security boundary (the agent logs a warning when it is set).
The agent sets up SKILL.md files (Agent Skills open standard) in .agents/skills/ directory:
- Scans
config.pluginsfor local plugins withpathfield - Checks
plugin_tools_dirparameter passed tostart() - Expands environment variables in paths (
$PLUGIN_PATH,${PLUGIN_PATH}) - Symlinks skill directories (falls back to copying if symlink fails)
- Codex CLI auto-discovers skills in
.agents/skills/
The Codex SDK is synchronous. The agent uses _run_async() helper to detect and await coroutines, preserving the async interface.
| Feature | Claude Code | Codex |
|---|---|---|
| SDK Type | Subprocess (CLI via JSON generator) | Sync client (app-server subprocess) |
| Command Tracking | Full telemetry (tool name, params, duration) | Streamed telemetry: shell → Bash, apply_patch → Write |
| Model Selection | Direct via --model or config |
agent.model pinned into thread_start |
| Session Resume | --resume {session_id} |
Via thread ID |
| Permissions | permission_mode + allowed_tools |
permission_mode → sandbox/approval + allowed_tools/disallowed_tools → thread config |
| Tool Enforcement | Not enforced by Coder Eval wrapper | enabled_tools honored; disabled_tools NOT enforced by the SDK |
| Early stop | Supported (cooperative should_stop, polled between messages) |
Supported — polled after each streamed notification; the in-flight turn is interrupted best-effort |
- Tool-name collapse - Codex reports shell tools (
Read/Grep/Bash) all as shell commands, surfaced asBashtelemetry; name-keyed criteria that distinguish these tools aren't meaningful across agents. skill_triggeredcriterion - Codex has no distinctSkilltool (it engages a skill by reading its files via shell), so the criterion detects Codex engagement from that file-read signal (a command referencingskills/<name>/) instead of aSkilltool call. The file-read signal is weaker than Claude's explicit invocation.disallowed_tools- passed to the SDK but not enforced; not a security boundary.- Authentication - Requires
CODEX_API_KEYin the environment (point it at whichever endpoint's key you use — OpenAI, gateway, or Azure); the agent callslogin_api_keywhen a key is present.OPENAI_API_KEY/AZURE_OPENAI_API_KEYare NOT read. - Model field -
TurnRecord.model_usedreflects the pinnedagent.model; the CodexTurnpayload itself doesn't carry the resolved model. - Skills with Windows paths - Symlink creation may fail on Windows; agent falls back to copying (slower).
- Implement session-based resume (thread ID tracking)
- Strengthen the Codex
skill_triggeredsignal — it currently infers engagement from a file read, weaker than Claude'sSkilltool call - Capture the resolved model from the SDK (vs. the pinned config value)
Run the included test tasks:
# Basic functionality test
coder-eval run tasks/agents/codex_hello_world.yaml
# Tool restriction test (verifies disallowed_tools enforcement)
coder-eval run tasks/agents/codex_disallowed_tools_test.yaml
# Skills discovery test (requires PLUGIN_PATH environment variable)
export PLUGIN_PATH=~/path/to/skills
coder-eval run tasks/agents/codex_skills_test.yamlExample unit test to verify agent setup:
import pytest
from coder_eval.models import AgentKind, AgentConfig
from coder_eval.agents.codex_agent import CodexAgent
from coder_eval.agent import AgentState
def test_codex_agent_initialization():
"""Verify CodexAgent can be instantiated with valid config."""
config = AgentConfig(
type=AgentKind.CODEX,
permission_mode="acceptEdits",
allowed_tools=["Bash", "Read", "Write"],
)
agent = CodexAgent(config)
assert agent.get_state() == AgentState.WORKING
assert agent.config.type == AgentKind.CODEX
def test_tool_name_mapping():
"""Verify Claude Code tool names map to Codex SDK names."""
from coder_eval.agents.codex_agent import _CLAUDE_TO_CODEX_TOOL_MAP
assert _CLAUDE_TO_CODEX_TOOL_MAP["Bash"] == "shell"
assert _CLAUDE_TO_CODEX_TOOL_MAP["Write"] == "apply_patch"
assert _CLAUDE_TO_CODEX_TOOL_MAP["Edit"] == "apply_patch"
assert _CLAUDE_TO_CODEX_TOOL_MAP["Read"] == "shell"
def test_permission_mode_mapping():
"""Verify permission_mode maps to a sandbox; approval is uniformly deny_all."""
from coder_eval.agents.codex_agent import (
_CODEX_APPROVAL_MODE,
_PERMISSION_MODE_TO_SANDBOX,
)
assert _PERMISSION_MODE_TO_SANDBOX["acceptEdits"] == "workspace-write"
assert _PERMISSION_MODE_TO_SANDBOX["plan"] == "read-only"
# Approval is the same for every permission mode — no per-mode mapping.
assert _CODEX_APPROVAL_MODE == "deny_all"