Codex Local Adapter
Status: [Live] — integrated with
setup.worker.tsand selectable per-agent via the Manage portal Agent edit page.client-researcheris configured to usecodex_localwith modelgpt-5.4by default.Official CLI reference: Codex CLI Command Line Options — authoritative flags, subcommands, and
codex execNDJSON stream documentation.Pricing: Codex Pricing — subscription plans (Free/Go/Plus/Pro) and API key token-based billing.
Skills: Agent Skills — how to author and place
SKILL.mdfiles for Codex to discover.
Overview
Mechanism: Spawns codex (OpenAI Codex CLI) as a child process. Communicates via NDJSON (newline-delimited JSON) on stdout.
Why Codex for code-heavy tasks:
- Native code generation and execution environment
- Session resumption via
thread_id— stateful multi-turn context across runs - Two auth modes: ChatGPT subscription (Plus/Pro/Business) or OpenAI API key
- GPT-5 family models with strong coding capabilities
When to use:
- Technical script generation (Google Ads scripts, spreadsheet macros)
- Data processing automation
- Code generation tasks where an agentic coding environment is preferred
client-researcheragent (default adapter) — research tasks using gpt-5.4
Differences vs Claude adapter:
- Skills work differently — Codex discovers
SKILL.mdfiles from.agents/skills/directories in thecwdtree; there is no--add-dirflag. See Skills below. - Cost tracking not yet wired —
turn.completedusage data is confirmed present and correct (verified April 2026 viatest-codex-tokens.mjs), but the worker does not yet store it. TODO: passusagethrough to the activity run record. - Tool call count not tracked (BullMQ worker emits 0)
- API key users are billed per token at standard OpenAI rates; subscription users pay $0 per run
Configuration
CodexLocalConfig (stored in agent_configs.adapter_config):
interface CodexLocalConfig {
cwd: string; // working directory; auto-created if absent
model?: string; // e.g. 'gpt-5.4-mini' (default: 'gpt-5.4')
promptTemplate?: string; // template with {{variables}} substitution
instructionsFilePath?: string; // path to Markdown file prepended to prompt
env?: Record<string, string>; // per-agent env vars
timeoutSec?: number; // hard timeout (default: 120)
graceSec?: number; // SIGTERM → SIGKILL grace period (default: 5)
dangerouslyBypassApprovalsAndSandbox?: boolean; // pass --dangerously-bypass-approvals-and-sandbox
}promptTemplate variable substitution:
| Variable | Value |
|---|---|
{{agentId}} | Agent identifier |
{{agent.name}} | e.g. "AI Copywriter" |
{{tenantId}} | Tenant identifier |
{{tenant.name}} | Tenant display name |
{{runId}} | This run’s ID |
Supported Models
| Model ID | Notes |
|---|---|
gpt-5.4 | Default — highest capability |
gpt-5.4-mini | Faster, cheaper |
gpt-5.3-codex | Codex-optimised reasoning |
gpt-5.2-codex | Previous Codex generation |
gpt-5.2 | GPT-5.2 base |
gpt-5.1-codex-max | Max context Codex |
gpt-5.1-codex-mini | Lightweight Codex |
Authentication options:
- ChatGPT subscription (Plus/Pro/Business/Enterprise) —
codex loginvia OAuth. Usage draws from plan limits/credits. Cost per run = $0.00.- OpenAI API key —
printenv OPENAI_API_KEY | codex login --with-api-key. Billed per token at standard API rates. Recommended for CI/automation. Note: API key auth has delayed access to newest models (e.g. GPT-5.3-Codex and GPT-5.3-Codex-Spark available on subscription first).
Full Subprocess Flow
Worker process (Node.js)
│
│ 1. ASSEMBLE INPUTS
│ renderTemplate(config.promptTemplate, ctx) → renderedPrompt
│ readFile(config.instructionsFilePath) → prepended to renderedPrompt
│
│ 2. SPAWN SUBPROCESS
│ ┌──────────────────────────────────────────────────────────┐
│ │ child_process.spawn('codex', args, { │
│ │ cwd: config.cwd, │
│ │ env: { ...process.env, ...config.env }, │
│ │ stdio: ['pipe', 'pipe', 'pipe'], ← stdin piped │
│ │ shell: process.platform === 'win32', │
│ │ }) │
│ └──────────────────────────────────────────────────────────┘
│
│ 3. DATA IN — prompt written to stdin, then stdin closed
│ proc.stdin.end(renderedPrompt)
│
│ CLI args for a new run:
│ codex exec --dangerously-bypass-approvals-and-sandbox (--yolo is an alias)
│ --json --skip-git-repo-check -C <cwd> -m <model> -
│ ↑ stdin sentinel
│
│ CLI args for session resume:
│ codex exec resume <thread_id> --json \
│ --dangerously-bypass-approvals-and-sandbox -m <model> -
│
│ 4. DATA OUT — read from child.stdout (NDJSON)
│ Each line is one complete JSON event.
│
└─────────────────────────────────────────────────────────────────The NDJSON event stream
// Session established (always first)
{ "type": "thread.started", "thread_id": "019d6d43-a3db-7ab0-b89e-e5823f145c1a" }
// Turn begins
{ "type": "turn.started" }
// Agent output — one event per message chunk
{ "type": "item.completed", "item": { "id": "item_0", "type": "agent_message", "text": "Here is the script..." } }
// Shell command starting (e.g. Codex reading a skill file or running bash)
{ "type": "item.started", "item": { "id": "item_1", "type": "command_execution", "command": "powershell -Command \"Get-Content ...SKILL.md\"", "status": "in_progress" } }
{ "type": "item.completed", "item": { "id": "item_1", "type": "command_execution", "command": "...", "aggregated_output": "...", "exit_code": 0, "status": "completed" } }
// Turn ends — usage data is here
{ "type": "turn.completed", "usage": { "input_tokens": 10706, "cached_input_tokens": 9088, "output_tokens": 70 } }
// Error (if any)
{ "type": "error", "message": "...", "code": "..." }Note:
item.started/item.completedpairs withtype: "command_execution"appear whenever Codex runs a shell command — including when it loads a skill file at runtime (implicit invocation). Theaggregated_outputfield holds the captured stdout of the command.
Key I/O summary
| Concern | How it’s handled |
|---|---|
| Passing the prompt | Written to stdin, then stdin.end() — avoids Windows arg-quoting issues with shell: true |
| Session resumption | codex exec resume <thread_id> subcommand — replaces -C <cwd> with thread ID |
| Getting streamed text | item.completed events where item.type === "agent_message" → join item.text |
| Getting the session ID | thread.started event → thread_id saved to sessions table |
| Getting token usage | turn.completed event → usage.input_tokens, usage.output_tokens, usage.cached_input_tokens |
| Approval bypass | --dangerously-bypass-approvals-and-sandbox (alias: --yolo) — required for unattended operation |
| Skill loading events | item.started + item.completed with type: "command_execution" — emitted when Codex reads a skill file or runs any shell command |
| Process timeout | SIGTERM at timeoutSec, SIGKILL at timeoutSec + graceSec; resolveOnce pattern prevents double-resolve on Windows |
Why stdin and not -p?
The - sentinel at the end of the args tells Codex to read the prompt from stdin. This avoids Windows quoting issues that arise when the prompt is passed as a CLI argument with shell: true (needed on Windows to find .cmd shims). The prompt is written to proc.stdin, then proc.stdin.end() closes stdin to signal end of input.
Why resolveOnce instead of resolve?
On Windows with shell: true, proc.kill("SIGKILL") kills the cmd.exe wrapper but not the child process. The close event may never fire after SIGKILL. To avoid hanging forever, the timeout handler force-resolves the promise 500ms after SIGKILL. The resolveOnce wrapper ensures the close handler can’t double-resolve if it does eventually fire.
Session Handling
- Session ID =
thread_idfrom thethread.startedevent - Persisted to
sessionstable - Subsequent runs:
codex exec resume <thread_id>— Codex resumes from its own local session store - If the session is unknown (e.g. after machine restart), adapter detects the error and clears the session ID so the next run starts fresh
Health Checks
The testEnvironment(config) function runs these checks in sequence:
| Check | What it verifies |
|---|---|
| CLI installed | codex --version exits 0 |
| cwd accessible | Config cwd exists (or will be created) |
| Live probe | Spawns codex exec --json --dangerously-bypass-approvals-and-sandbox - with "Respond with: hello" on stdin; expects a response within 20s |
Timeout Handling
Two-stage graceful shutdown identical to the Claude adapter:
- At
timeoutSec: send SIGTERM — Codex flushes and exits - After
graceSec(default 5s): send SIGKILL — hard kill - 500ms after SIGKILL: force-resolve via
resolveOnce(Windowsshell: truesafety net)
Cost Source
Token counts come from the turn.completed event → usage.input_tokens, usage.output_tokens, usage.cached_input_tokens.
Cost depends on auth mode:
| Auth mode | Cost per run |
|---|---|
| ChatGPT subscription (Plus/Pro/Business) | $0.00 — usage deducted from plan credits |
| OpenAI API key | Calculated from MODEL_PRICING × token counts |
Cost calculation uses OpenAI model pricing (same MODEL_PRICING table as the OpenAI adapter).
Verified April 2026 via scripts/test-codex-tokens.mjs:
turn.completedreliably contains all three usage fields- Baseline prompt (gpt-5.4-mini): ~10,483 input / 21 output / 9,088 cached = $0.0051 API-equivalent
- High cache-hit rate (~87%) driven by Codex’s own system prompt being stable across runs
Skills
Codex discovers skills from SKILL.md files placed in .agents/skills/ directories — it does not use a --add-dir CLI flag.
Discovery locations (highest to lowest priority):
| Scope | Path | Use case |
|---|---|---|
| Repo (local) | $CWD/.agents/skills/ | Skills for a specific module or service |
| Repo (parent) | $CWD/../.agents/skills/ | Shared skills in a parent folder |
| Repo (root) | $REPO_ROOT/.agents/skills/ | Organisation-wide repo skills |
| User | $HOME/.agents/skills/ | Personal skills across all repos |
| Admin | /etc/codex/skills/ | Machine/container defaults |
| System | Bundled with Codex | Built-in skills (e.g. $skill-creator) |
Skill format — a directory containing a SKILL.md:
my-skill/
SKILL.md # required — name, description, instructions
scripts/ # optional — executable code
references/ # optional — docs/context files
assets/ # optional — templates
agents/
openai.yaml # optional — UI metadata, invocation policy---
name: keyword-research
description: Use this skill when the task involves SEO keyword research or search intent analysis.
---
Instructions for Codex to follow...How Codex uses skills (verified behaviour — April 2026):
-
Explicit invocation — prompt includes
$skill-name. Codex loads and follows the skill immediately. Fast path: ~1 LLM turn, minimal extra tokens."Please run $lm-test-skill now." → item.completed { type: "agent_message", text: "SKILL_ACTIVE_CONFIRMED ..." } → turn.completed { input: 10,706 / output: 70 / cached: 9,088 } -
Implicit invocation — prompt matches the skill
description. Codex:- Reads all skill metadata (
name,description) upfront from the directory listing - Decides the skill applies
- Executes a shell command at runtime to read the
SKILL.mdfile (Get-Contenton Windows,caton Linux) - Follows the instructions in the file
"Run the leadmetrics skill test for me." → item.completed { type: "agent_message", text: "Using lm-test-skill..." } → item.started { type: "command_execution", command: "powershell Get-Content ...SKILL.md" } → item.completed { type: "command_execution", aggregated_output: "---\nname: lm-test-skill\n..." } → item.completed { type: "agent_message", text: "SKILL_ACTIVE_CONFIRMED ..." } → turn.completed { input: 21,397 / output: 222 / cached: 20,736 } - Reads all skill metadata (
Token cost difference: Implicit invocation costs roughly 2× the tokens of explicit invocation because of the extra shell command round-trip to read the skill file. Prefer explicit
$skill-namereferences in agent prompts when token efficiency matters.
Adapter integration: Place skills in $config.cwd/.agents/skills/ — Codex picks them up automatically since cwd is passed as -C <cwd>. No adapter-level changes needed.
Reference: developers.openai.com/codex/skills
Package Location
packages/adapters/codex-local/
├── src/
│ ├── index.ts # type key, label, models, defaultModel, agentConfigurationDoc
│ ├── types.ts # CodexLocalConfig, stream event types, AdapterExecutionContext
│ ├── server/
│ │ ├── execute.ts # buildArgs(), renderTemplate(), execute()
│ │ ├── parse.ts # parseNdjsonLines(), extractSessionId(), extractOutput(), extractUsage(), buildTranscript()
│ │ ├── test.ts # testEnvironment() — CLI probe + diagnostics
│ │ └── __tests__/
│ │ ├── execute.test.ts # unit tests — buildArgs, renderTemplate
│ │ ├── parse.test.ts # unit tests — parse/extract/buildTranscript
│ │ ├── build-config.test.ts# unit tests — buildConfig, validateConfig
│ │ └── integration/
│ │ └── execute.integration.test.ts # live codex CLI tests
│ └── ui/
│ ├── build-config.ts # configFields[], buildConfig(), validateConfig()
│ └── __tests__/
│ └── build-config.test.ts
├── vitest.config.ts
└── package.jsonTest status: 44/44 unit tests passing, 7/7 integration tests passing.
Exploratory scripts (in scripts/):
| Script | What it tests |
|---|---|
test-codex-tokens.mjs | Token usage & cost: runs a minimal prompt, prints turn.completed usage breakdown |
test-codex-skills.mjs | Skills: creates a temp .agents/skills/ dir, runs explicit + implicit invocation, asserts magic-word output |