Codex¶
Codex is the harness's second first-class agent, and the one the agent contract was frozen against. Everything on this page was measured on codex-cli 0.154.0; the adapter is pinned to that version with no version switch, which is a deliberate limit rather than an oversight.
[profiles.cx]
agent = "codex"
config_dir = "~/.codex-cx"
roots = ["~/code/experiments"]
What the harness writes¶
| Path | Owned how |
|---|---|
$CODEX_HOME/AGENTS.md |
Assembled from the profile's segments; carries a GENERATED by header |
$CODEX_HOME/hooks.json |
Shared. Merged per hook group: the harness owns only the groups it can prove are its own builtins; foreign groups and event order are preserved |
$CODEX_HOME/config.toml |
Shared. Only [mcp_servers.*] is merged, with tomlkit |
config.toml being shared is the interesting one. It is Codex's file and the
user's, so the merge carries [projects.*] trust entries and [hooks.state]
approvals through untouched. MCP servers go here rather than into a dedicated
document — mcp_config_file() returns "" for Codex, and the planner is the
oracle for the behaviour, not that method.
Hook trust¶
Codex requires a deployed hooks.json to be trusted before it will run the
hooks in it. lh deploy prints the re-trust instruction whenever a declaration
changes, and lh doctor reports the state per Codex profile.
It reports two states where Codex's own config records three —
untrusted, unknown, and orphaned entries — and never reports trusted.
The reason is worth stating plainly: telling trusted from modified would mean
recomputing Codex's own hash, which means reimplementing its TOML normalisation
and going silently wrong the first time that normalisation changes upstream.
Reading trusted_hash back only proves a hash was stored, not that it still
matches the current content. So a stored hash reports as unknown.
Untrusted is the expected state immediately after a deploy, so this section
never fails lh doctor. It is silent when no profile runs Codex.
One measurement stands on one data point: the hashes stored by 0.154.0 were
honoured by 0.155.0 without re-approval. The roadmap item re-opens on the first
upgrade that asks again for an unchanged hooks.json.
Two edit paths, and only one is gateable¶
Codex edits files through two paths, chosen non-deterministically:
apply_patch— its own tool, carrying a*** Begin Patch … *** End Patchblob. The adapter parses the blob intoFileEdits, maps the tool tomodify_file, and — since ADR-046 — reports deletions in a separatedeletesfield rather than as a kind of edit. Security hooks evaluate every path in a call, which closed a hole where a multi-file patch left the second path ungated.Bash— a heredoc, under the sametool_input.commandkey. This path is structurally ungateable: the input is an arbitrary shell script with no path to extract.
So a fully-correct adapter gates roughly half of Codex's edits. ADR-044 records that as a measured consequence, not a TODO.
pre-tool-use-read-size is inert on Codex for a different reason: its tool map
has no read tool at all, so the operation it reasons about never arrives.
lh doctor's Hook operations section names it.
Matchers are not forked per agent¶
The deployed PreToolUse groups keep their Claude-shaped matcher literals —
Bash|Read|Edit|Write|NotebookEdit — unchanged. Probes showed Codex matches
them by regex with tool-name aliasing (apply_patch matches Edit|Write) and
evaluates every group rather than stopping at the first match, so there is
nothing to fork
(ADR-056).
That ADR is also a good example of the repo's own failure discipline. An acceptance run reported every hook approved and still failed three assertions. The cause turned out not to be trust at all: the recursive-delete rule required recursion and force, and the model had issued a spelling with only recursion. The rule now denies on recursion alone.
Metering¶
Codex is metered through its TranscriptReader, the same Protocol Claude Code
uses — there is no dialect-specific parser in the ingest pipeline any more
(ADR-053).
- Transcripts live at
$CODEX_HOME/sessions/<yyyy>/<mm>/<dd>/rollout-<ts>-<uuid>.jsonl. - Messages and tool calls come from the
response_itemstream; token usage fromtoken_usage_record. Theevent_msg/*stream is skipped entirely — reading both would duplicate every fact. - Only
userandassistantroles are kept.developeris the instruction channel, re-sent every turn. - Dedup keys on
response_id, which arrives through the samemessage_idfield as Claude Code'smessage.id. sincefilters on the file'sst_mtime. The timestamp in the filename is deliberately ignored: it is local start time with no offset.
Cost. A Codex login on a ChatGPT plan is a subscription, not per-token
billing. Set billing_model = "flat_rate" on the profile and its rows render
— rather than a fabricated $0.00, and stay out of unknown_models. With
billing_model = "per_token" and no published rate for the model id the reader
reports, the row lands in unknown_models — which is the honest outcome, not a
bug. See cost reporting.
Launching¶
lh run --profile cx
lh run --profile cx --bypass=activate # → --approve-for-me
lh run --profile cx --bypass=no-sandbox # → --dangerously-bypass-approvals-and-sandbox
lh run --profile cx --bypass=enable # refused: Codex has no such level
--bypass=enable is refused rather than answered with activate, because
"available but off" and "on" are different grants. Codex's
--dangerously-bypass-hook-trust is never emitted by the harness: it is a
different axis — trust in the harness's own hooks, not model approval.
lh exec does not support Codex today; HeadlessAgent is implemented only by
the Claude Code adapter.
Credentials¶
credentials_file() returns None, so session-start preflight reports n/a
for Codex auth — "this check does not apply to your agent", which is a
different finding from "this check could not tell".
auth.json's last_refresh is deliberately not turned into a freshness
verdict. No threshold at 7, 30 or 90 days was defensible, because nothing
correlates that field with expiry
(ADR-057).
Known limits¶
| Limit | Why it stands |
|---|---|
Bash edits are ungateable |
The tool input is an arbitrary shell script |
pre-tool-use-read-size is inert |
No read tool in the agent's tool map |
stop-verify-guard is never deployed |
No rollout record marks an explicit goal |
Hook trust never reports trusted |
Would require reimplementing Codex's TOML normalisation |
| Pinned to codex-cli 0.154.0 | No version switch; a newer dialect is unmeasured |
No lh exec |
HeadlessAgent not implemented |