Skip to Content
AdaptersCodex Local Adapter

Codex Local Adapter

Status: [Live] — integrated with setup.worker.ts and selectable per-agent via the Manage portal Agent edit page. client-researcher is configured to use codex_local with model gpt-5.4 by default.

Official CLI reference: Codex CLI Command Line Options  — authoritative flags, subcommands, and codex exec NDJSON 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.md files 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-researcher agent (default adapter) — research tasks using gpt-5.4

Differences vs Claude adapter:

  • Skills work differently — Codex discovers SKILL.md files from .agents/skills/ directories in the cwd tree; there is no --add-dir flag. See Skills below.
  • Cost tracking not yet wired — turn.completed usage data is confirmed present and correct (verified April 2026 via test-codex-tokens.mjs), but the worker does not yet store it. TODO: pass usage through 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:

VariableValue
{{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 IDNotes
gpt-5.4Default — highest capability
gpt-5.4-miniFaster, cheaper
gpt-5.3-codexCodex-optimised reasoning
gpt-5.2-codexPrevious Codex generation
gpt-5.2GPT-5.2 base
gpt-5.1-codex-maxMax context Codex
gpt-5.1-codex-miniLightweight Codex

Authentication options:

  • ChatGPT subscription (Plus/Pro/Business/Enterprise) — codex login via OAuth. Usage draws from plan limits/credits. Cost per run = $0.00.
  • OpenAI API keyprintenv 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.completed pairs with type: "command_execution" appear whenever Codex runs a shell command — including when it loads a skill file at runtime (implicit invocation). The aggregated_output field holds the captured stdout of the command.

Key I/O summary

ConcernHow it’s handled
Passing the promptWritten to stdin, then stdin.end() — avoids Windows arg-quoting issues with shell: true
Session resumptioncodex exec resume <thread_id> subcommand — replaces -C <cwd> with thread ID
Getting streamed textitem.completed events where item.type === "agent_message" → join item.text
Getting the session IDthread.started event → thread_id saved to sessions table
Getting token usageturn.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 eventsitem.started + item.completed with type: "command_execution" — emitted when Codex reads a skill file or runs any shell command
Process timeoutSIGTERM 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_id from the thread.started event
  • Persisted to sessions table
  • 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:

CheckWhat it verifies
CLI installedcodex --version exits 0
cwd accessibleConfig cwd exists (or will be created)
Live probeSpawns 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:

  1. At timeoutSec: send SIGTERM — Codex flushes and exits
  2. After graceSec (default 5s): send SIGKILL — hard kill
  3. 500ms after SIGKILL: force-resolve via resolveOnce (Windows shell: true safety 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 modeCost per run
ChatGPT subscription (Plus/Pro/Business)$0.00 — usage deducted from plan credits
OpenAI API keyCalculated 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.completed reliably 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):

ScopePathUse 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
SystemBundled with CodexBuilt-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:

    1. Reads all skill metadata (name, description) upfront from the directory listing
    2. Decides the skill applies
    3. Executes a shell command at runtime to read the SKILL.md file (Get-Content on Windows, cat on Linux)
    4. 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 }

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-name references 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.json

Test status: 44/44 unit tests passing, 7/7 integration tests passing.

Exploratory scripts (in scripts/):

ScriptWhat it tests
test-codex-tokens.mjsToken usage & cost: runs a minimal prompt, prints turn.completed usage breakdown
test-codex-skills.mjsSkills: creates a temp .agents/skills/ dir, runs explicit + implicit invocation, asserts magic-word output

© 2026 Leadmetrics — Internal use only