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
codexon$PATHor pointed to via$CODEX_PATHcodex loginalready run once (the runner symlinks~/.codex/auth.jsoninto 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>]
| Flag | Type | Default | Description |
|---|---|---|---|
--no-trace | bool | off | Disable trace capture: no MITM proxy, no per-session CA, no env injection. The runner still does briefing + IPC + channel routing normally. |
--cwd <dir> | path | process.cwd() | Working directory for codex. Codex’s apply-patch and shell tools resolve paths relative to this. |
--model <name> | string | codex picks | Forwarded into thread/start as model. Codex’s default model selection takes over when omitted. |
--url <url> | string | http://127.0.0.1:8717 | Broker base URL. Falls back to $AC7_URL. |
--token <secret> | string | — | Bearer token. Falls back to $AC7_TOKEN then ~/.config/ac7/auth.json. |
What it does on startup
-
Locate
codex. Reads$CODEX_PATHfirst, falls back towhich codex. Fails fast if missing, with an install hint (npm i -g @openai/codex). -
Start the runner. Same
startRunner()claude-code uses, but passes a customnotificationSink: 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. -
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 withdefault_tools_approval_mode = "approve"
-
Spawn
codex app-serverwithCODEX_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. -
Wire the JSON-RPC client on top of stdio. Codex’s wire format is newline-delimited JSON, one message per line, and the
jsonrpcfield is omitted — the client routes by message shape (id+method= request,id+result|error= response,methodonly = notification). -
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 setapprovalPolicy: never, but if they do, denying is safer than blocking). -
initializehandshake. Codex requires this before any other method. -
thread/startcarrying our briefing asdeveloperInstructionsand these explicit settings:Param Value Why cwd--cwdvalue orprocess.cwd()Where codex’s apply-patch/shell tools operate developerInstructionsthe runner briefing Pinned into every model context — analog of --append-system-promptmodel--modelvalue or omitCodex’s default takes over if omitted approvalPolicyneverHeadless: no UI to elicit against sandboxdanger-full-accessMirrors claude-code’s posture ( --dangerously-skip-permissionshas no codex equivalent that’s tighter; tighter modes (workspace-write,read-only) will land as a flag later) -
Drain the pre-attach buffer — any broker events queued by the sink wrapper while we were spinning up are flushed now, in order.
-
Hold until codex exits (or SIGINT/SIGTERM/runner shutdown), then tear down: flush the channel sink,
turn/interruptif a turn is active, close the JSON-RPC client, kill codex if still alive,rm -rfthe 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:
| Status | Buffer | Dispatch |
|---|---|---|
notLoaded | yes (indefinitely) | flush when status flips off |
idle | yes (200ms window) | turn/start with the bundled prose |
active | yes (200ms window) | turn/steer with expectedTurnId of the live turn |
systemError | drop | log 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):
| Method | Purpose |
|---|---|
initialize | Handshake; required before any other method |
thread/start | Open a fresh thread with developerInstructions + settings |
thread/resume | Resume a thread by id (not used today) |
turn/start | Start a fresh turn with input (used by channel sink when idle) |
turn/steer | Inject input into the live turn (used when active) |
turn/interrupt | Cancel 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: neverandsandbox: danger-full-accessare pinned. opaque_httptraces. 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.