ac7 codex

ac7 codex runs OpenAI Codex headlessly under codex app-server. There’s no TUI in your terminal — you direct the agent through the broker (chat / DMs / objectives / ac7 push), and the agent’s outputs flow back through the same channels. Useful for long-running agents on a workstation, CI workers, or remote VMs where there’s nobody at the keyboard.

ac7 codex

Prerequisites

  • codex on $PATH or pointed to via $CODEX_PATH
  • codex login already run once (the runner symlinks ~/.codex/auth.json into its ephemeral home — no auth.json, no OpenAI access)
npm install -g @openai/codex
codex login

Synopsis

ac7 codex [--no-trace] [--cwd <dir>] [--model <name>]
          [--url <url>] [--token <secret>]
FlagTypeDefaultDescription
--no-tracebooloffDisable trace capture: no MITM proxy, no per-session CA, no env injection. The runner still does briefing + IPC + channel routing normally.
--cwd <dir>pathprocess.cwd()Working directory for codex. Codex’s apply-patch and shell tools resolve paths relative to this.
--model <name>stringcodex picksForwarded into thread/start as model. Codex’s default model selection takes over when omitted.
--url <url>stringhttp://127.0.0.1:8717Broker base URL. Falls back to $AC7_URL.
--token <secret>stringBearer token. Falls back to $AC7_TOKEN then ~/.config/ac7/auth.json.

What it does on startup

  1. Locate codex. Reads $CODEX_PATH first, falls back to which codex. Fails fast if missing, with an install hint (npm i -g @openai/codex).

  2. Start the runner. Same startRunner() claude-code uses, but passes a custom notificationSink: a buffering wrapper that queues broker events until codex is ready, then drains them into the live channel sink. This closes the cold-start gap — any DMs that arrive while codex is initializing are still delivered.

  3. Set up the ephemeral CODEX_HOME. A fresh directory under ~/.cache/agentc7/codex/ac7-codex-<random>/ (override via $XDG_CACHE_HOME). Inside:

    • auth.json — symlinked to ~/.codex/auth.json (so OAuth refreshes from the real codex login persist)
    • config.toml — minimal: just our [mcp_servers.ac7] block with default_tools_approval_mode = "approve"
  4. Spawn codex app-server with CODEX_HOME=<ephemeral-dir> in its env, no extra flags. The runner owns its stdin/stdout for JSON-RPC; stderr is 'inherit' so codex’s diagnostics reach your terminal.

  5. Wire the JSON-RPC client on top of stdio. Codex’s wire format is newline-delimited JSON, one message per line, and the jsonrpc field is omitted — the client routes by message shape (id + method = request, id + result|error = response, method only = notification).

  6. Register handlers for codex’s notifications (thread/started, thread/status/changed, turn/started, turn/completed, item/started, item/completed, error, warning) and a defensive auto-deny for any approval / elicitation server-request (these shouldn’t fire because we set approvalPolicy: never, but if they do, denying is safer than blocking).

  7. initialize handshake. Codex requires this before any other method.

  8. thread/start carrying our briefing as developerInstructions and these explicit settings:

    ParamValueWhy
    cwd--cwd value or process.cwd()Where codex’s apply-patch/shell tools operate
    developerInstructionsthe runner briefingPinned into every model context — analog of --append-system-prompt
    model--model value or omitCodex’s default takes over if omitted
    approvalPolicyneverHeadless: no UI to elicit against
    sandboxdanger-full-accessMirrors claude-code’s posture (--dangerously-skip-permissions has no codex equivalent that’s tighter; tighter modes (workspace-write, read-only) will land as a flag later)
  9. Drain the pre-attach buffer — any broker events queued by the sink wrapper while we were spinning up are flushed now, in order.

  10. Hold until codex exits (or SIGINT/SIGTERM/runner shutdown), then tear down: flush the channel sink, turn/interrupt if a turn is active, close the JSON-RPC client, kill codex if still alive, rm -rf the ephemeral CODEX_HOME.

Why ephemeral CODEX_HOME

~/.codex/config.toml is the user’s HOME-level config; it likely has MCP servers, profile defaults, and other state we shouldn’t merge into. Multi-runner runs would race on the same file. Backup-and-restore for HOME-level state is also scarier than for per-project state — a botched restore loses real config.

So every ac7 codex invocation gets its own short-lived CODEX_HOME which the runner owns end-to-end. Codex reads everything (auth, config, sessions) from that root via the CODEX_HOME env var, so the user’s real ~/.codex is never touched. The auth.json symlink is the one piece we share; on cleanup the symlink is removed but the real file isn’t.

The cache lives under ~/.cache/agentc7/codex/, not $TMPDIR. Codex refuses to install helper binaries (apply-patch, etc.) under tmpfs and emits a warning that disables them. Cache directories don’t trigger that.

The MCP bridge wiring

Same model as claude-code: the runner detects the bridge command from its own process and writes the bridge invocation into config.toml:

# Auto-generated by ac7 codex runner — do not edit.
# Lifetime: this entire CODEX_HOME directory is ephemeral.

[mcp_servers.ac7]
command = "/path/to/node"
args = ["/path/to/cli/dist/index.js", "mcp-bridge"]
enabled = true
default_tools_approval_mode = "approve"

[mcp_servers.ac7.env]
AC7_RUNNER_SOCKET = "/tmp/.ac7-runner-12345.sock"

default_tools_approval_mode = "approve" is the snake-case enum codex uses for “always auto-approve every call from this server.” The team’s permission model is the access control — per-tool approval prompts would block headless runs forever.

Channel sink: turn/start vs turn/steer

Broker push events reach codex through a channel sink that the runner forwarder dispatches into. Routing depends on codex’s current ThreadStatus:

StatusBufferDispatch
notLoadedyes (indefinitely)flush when status flips off
idleyes (200ms window)turn/start with the bundled prose
activeyes (200ms window)turn/steer with expectedTurnId of the live turn
systemErrordroplog line

Bundling matters because each turn/steer adds a user-input item the model sees on its next API call. A flurry of broker events landing simultaneously (e.g. ten objective updates) collapse into one steer with all the prose concatenated, instead of ten separate steers each costing a model-side awareness slot.

The 200ms window is short enough that a director typing in chat sees their message reach the agent within one round-trip; long enough that bursty objective lifecycle changes coalesce.

The race (turn/steer mismatch)

Codex returns an error if the expectedTurnId we steer with no longer matches the active turn — the turn ended between our flush decision and the dispatch arriving server-side. The sink retries once: re-read status and either dispatch as turn/start (now idle) or turn/steer with the new turn id. If the second attempt also fails the events drop with a log line; that’s almost always thread-shutdown anyway.

Channel framing

Each broker event is rendered as a single <channel>-tagged block the agent can recognize as ambient signal rather than fresh user input:

<channel kind="chat" from="director" thread="general" level="info" ts="04/15/26 14:23:45 UTC" msg_id="msg-...">
  pull latest main and run smoke tests
</channel>

The framing mirrors what claude-code’s notifications/claude/channel MCP notification produces, just rendered as text inside a turn/start / turn/steer UserInput item. Reserved meta keys are quoted as attributes; arbitrary data.* keys from the push payload land as additional attributes.

JSON-RPC subset codex uses

Methods (client → server):

MethodPurpose
initializeHandshake; required before any other method
thread/startOpen a fresh thread with developerInstructions + settings
thread/resumeResume a thread by id (not used today)
turn/startStart a fresh turn with input (used by channel sink when idle)
turn/steerInject input into the live turn (used when active)
turn/interruptCancel the active turn (used during shutdown)

Notifications (server → client):

thread/started, thread/status/changed, thread/closed, turn/started, turn/completed, item/started, item/completed, item/agentMessage/delta, account/rateLimits/updated, error, warning.

Server requests (auto-denied if they fire): item/commandExecution/requestApproval, item/fileChange/requestApproval, item/permissions/requestApproval, item/tool/requestUserInput, mcpServer/elicitation/request.

For the canonical wire types see packages/cli/src/runtime/agents/codex/protocol.ts.

Environment passed to codex

Codex’s HTTP client is reqwest, not Node — the env vars the runner injects when tracing is on differ from the claude-code set:

CODEX_HOME=<ephemeral-dir>
HTTPS_PROXY=http://127.0.0.1:<ephemeral-port>
HTTP_PROXY=http://127.0.0.1:<ephemeral-port>
ALL_PROXY=http://127.0.0.1:<ephemeral-port>
NO_PROXY=localhost,127.0.0.1,::1,<caller's value>
CODEX_CA_CERTIFICATE=$TMPDIR/ac7-trace-ca-<pid>-<nonce>.pem
SSL_CERT_FILE=$TMPDIR/ac7-trace-ca-<pid>-<nonce>.pem

NODE_EXTRA_CA_CERTS and NODE_USE_ENV_PROXY are deleted from the inherited env — they’re Node-only and would confuse codex. CODEX_CA_CERTIFICATE is the canonical knob; SSL_CERT_FILE is a fallback for the bundled-roots path.

When --no-trace is set the runner only sets CODEX_HOME.

Trace coverage today

Codex’s HTTPS traffic flows through the same MITM proxy that claude-code uses, and individual exchanges land in the activity stream just as readily. The current limitation is parsing: every exchange comes in as opaque_http because the typed parser only recognizes Anthropic’s /v1/messages shape. Adding an OpenAI parser (so codex traces render the same way claude-code traces do — model, tokens, messages with tool_use blocks) is a follow-up.

In the meantime directors can review codex traces as opaque HTTP records: host, method, URL, status, header + body previews, with the same redaction patterns applied. The information is there; the rendering is uglier.

Common patterns

# Cold start a new device
ac7 connect
codex login            # if not already done
ac7 codex

# Pin a specific model
ac7 codex --model gpt-5

# Run codex on a specific working tree
ac7 codex --cwd ~/projects/widget-service

# Disable trace (debugging IPC plumbing)
ac7 codex --no-trace

What you give up vs claude-code

  • No TUI. Direction is broker-only.
  • No HUD strip. A one-line “agent connected” notice replaces it. Use the web UI for live status.
  • No interactive approval. Headless means anything that would prompt blocks instead — that’s why approvalPolicy: never and sandbox: danger-full-access are pinned.
  • opaque_http traces. Until a typed parser lands.

What you gain: one broker, one identity, the same MCP toolbox, and no terminal pinned to a single agent. Spin up codex on a remote VM, push objectives at it, walk away.