Running OpenAI Codex in Coder Eval
Overview
Section titled “Overview”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.
1. Install the Codex SDK
Section titled “1. Install the Codex SDK”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)
2. Authentication
Section titled “2. Authentication”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 Flowawait 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.)
Endpoint routing
Section titled “Endpoint routing”| 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/openaiCODEX_API_VERSION=2025-04-01-preview # required by AzureCODEX_MODEL=<your-deployment-name> # deployment, not the base model idCODEX_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.
Command Line
Section titled “Command Line”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 codexTask Definition (YAML)
Section titled “Task Definition (YAML)”Specify 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
Skills (SKILL.md)
Section titled “Skills (SKILL.md)”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-agentscoder-eval run tasks/my_task.yamlArchitecture
Section titled “Architecture”Class Hierarchy
Section titled “Class Hierarchy”Agent (ABC)└── CodexAgent ├── Codex SDK Client (openai_codex.Codex) ├── Thread Management (thread_start, turn.stream) └── Streaming telemetry (commands, token usage, agent text)Key Methods
Section titled “Key Methods”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
TurnRecord Format
Section titled “TurnRecord Format”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
Implementation Details
Section titled “Implementation Details”Timeout Handling
Section titled “Timeout Handling”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.
Error Recovery
Section titled “Error Recovery”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
Permission and Tool Mapping
Section titled “Permission and Tool Mapping”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).
Skills Discovery
Section titled “Skills Discovery”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/
Async Integration
Section titled “Async Integration”The Codex SDK is synchronous. The agent uses _run_async() helper to detect and await coroutines, preserving the async interface.
Differences from Claude Code Agent
Section titled “Differences from Claude Code Agent”| 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 |
Known Limitations
Section titled “Known Limitations”- 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).
Future Enhancements
Section titled “Future Enhancements”- 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)
Testing
Section titled “Testing”Run the included test tasks:
# Basic functionality testcoder-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/skillscoder-eval run tasks/agents/codex_skills_test.yamlExample unit test to verify agent setup:
import pytestfrom coder_eval.models import AgentKind, AgentConfigfrom coder_eval.agents.codex_agent import CodexAgentfrom 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"