---
{
  "n": 9,
  "title": "Codex hooks - config and payloads, measured",
  "abstract": "What a Codex hook is given and may answer, read from a live codex-cli 0.142.5 run: the hooks.json shape, the stdin fields per event, and the decisions that worked",
  "refs": [],
  "seen": [
    "agent"
  ],
  "data": {},
  "created": 1789725491.0,
  "updated": 1789725491.0,
  "deleted": 0.0,
  "completed": 1789745298.543092,
  "outcome": "",
  "type": "doc"
}
---
Measured on 2026-09-18 against codex-cli 0.142.5 with `codex exec --dangerously-bypass-hook-trust -s workspace-write`, a hook script that dumped its stdin to a file and answered per event, in a scratch git repo with `.codex/hooks.json`. Every event fired that could fire in an exec run: SessionStart, UserPromptSubmit, PreToolUse (twice: one denied, one allowed), PostToolUse, Stop (twice: blocked, then re-entered with stop_hook_active true). SessionEnd did not fire for an exec run. The headline: the payload and the decision JSON are the same shape Claude Code's hooks use, field for field, so hook.py needs almost no adapter for Codex — what differs is the transcript (a rollout file under ~/.codex/sessions) and the trust step.

## The config: hooks.json or config.toml, three levels
Read from `~/.codex/hooks.json` or `~/.codex/config.toml`, then `<repo>/.codex/hooks.json` or `<repo>/.codex/config.toml` (the docs at learn.chatgpt.com/docs/hooks; the project file is what the probe used). Event → matcher group → handlers:

    {"hooks": {"PreToolUse": [{"matcher": "", "hooks": [{"type": "command", "command": "/abs/path/hook.py", "timeout": 10}]}]}}

The TOML twin is `[[hooks.PreToolUse]]` with `matcher = ""` and `[[hooks.PreToolUse.hooks]]` with type/command/timeout. Options per handler: `timeout` (seconds; default 600, 1 for SessionEnd and Interrupt), `statusMessage`, `async`, `additionalContextLimit` (2500 tokens before spilling to disk), `commandWindows`. The matcher is a regex on the tool name (PreToolUse/PostToolUse), the source (SessionStart: startup|resume), the trigger (PreCompact: manual|auto); "" or "*" matches all. Events: PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, UserPromptSubmit, SubagentStop, Stop (turn-scoped); SessionStart, SessionEnd, SubagentStart, Interrupt (session-scoped). The command runs OUTSIDE the sandbox: the probe wrote its dump file under a read-restricted sandbox without complaint. The TUI prints "hook: <Event>" / "hook: <Event> Completed|Blocked" for each run.

Trust: a user or project hook is not run until trusted — `/hooks` in the TUI, or `--dangerously-bypass-hook-trust` on `codex` and `codex exec` alike (both accept it; it prints a warning line twice). `[features] hooks = false` disables all of them; `allow_managed_hooks_only = true` ignores user and project hooks.

## The stdin payload, per event — Claude Code's fields
Every event carries: `session_id` (a UUIDv7), `transcript_path` (the rollout file, e.g. ~/.codex/sessions/2026/09/18/rollout-2026-09-18T11-56-48-<session_id>.jsonl — its stem is NOT the session id, it is rollout-<time>-<id>), `cwd`, `hook_event_name`, `model` ("gpt-5.5"), `permission_mode` ("bypassPermissions" under exec with the sandbox flag; also default, acceptEdits, plan, dontAsk). Turn-scoped events add `turn_id`.

    SessionStart      + source: "startup"
    UserPromptSubmit  + prompt
    PreToolUse        + tool_name: "Bash", tool_input: {"command": "echo forbidden > probe.txt"}, tool_use_id: "call_…"
    PostToolUse       + tool_name, tool_input, tool_response (a string; "" for a command with no output), tool_use_id
    Stop              + stop_hook_active: false|true, last_assistant_message: "done"

No environment variables named CODEX reach the hook, and it gets no argv. Measured payloads (session_id and paths shortened):

    {"session_id":"01a0b3f2-…","transcript_path":"…/rollout-2026-09-18T11-56-48-01a0b3f2-….jsonl","cwd":"…/codexprobe","hook_event_name":"SessionStart","model":"gpt-5.5","permission_mode":"bypassPermissions","source":"startup"}
    {"session_id":"…","turn_id":"01a0b3f2-7dd5-…","transcript_path":"…","cwd":"…","hook_event_name":"PreToolUse","model":"gpt-5.5","permission_mode":"bypassPermissions","tool_name":"Bash","tool_input":{"command":"echo forbidden > probe.txt"},"tool_use_id":"call_dZLGeT6WMHgYEXkn3tUsZuve"}
    {"session_id":"…","turn_id":"…","transcript_path":"…","cwd":"…","hook_event_name":"Stop","model":"gpt-5.5","permission_mode":"bypassPermissions","stop_hook_active":false,"last_assistant_message":"done"}

The shell tool is named `Bash` with `tool_input.command`, exactly as in Claude Code, so hook.py's `_is_write`, `_pieces` and the journal-verb detection read a Codex call unchanged.

## The answers that worked: deny, context, block
All on stdout, exit 0, the same JSON Claude Code's hooks use:

    PreToolUse deny     {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "…"}}
                        → the command is not run; Codex logs "Command blocked by PreToolUse hook: <reason>. Command: <cmd>" and the model reads the reason (it then ran the command the reason suggested).
    PostToolUse context {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "…"}}   → accepted ("Completed")
    SessionStart        {"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "…"}}   → the model acted on it in its first reply (measured: it answered PROBE-SEEN instead of running the command it was asked to run)
    Stop block          {"decision": "block", "reason": "…"}   → "hook: Stop Blocked", the model continues with the reason as its next prompt, and the next Stop arrives with stop_hook_active: true

Also in the docs, not exercised: exit 2 with stderr is a blocking decision; `updatedInput` on a PreToolUse allow rewrites the call; PermissionRequest answers `{"decision": {"behavior": "allow"|"deny"}}`; a PostToolUse block replaces the tool output; `continue`, `stopReason` and `systemMessage` are top-level fields.

What follows for the journal (to-dos 37, 38): hook.py can be installed as-is under `.codex/hooks.json` for the events it handles — `install.py` writes the same command lines it writes into `.claude/settings.json`, in the three-level shape above; the one real adaptation is the session's stem (rollout-…), which `_ctx` already derives from `transcript_path`, and reading that transcript (phase 5). `journal codex` should pass `--dangerously-bypass-hook-trust` or the user trusts the hooks once with /hooks.

## What hook.py does with it (to-do 37)
No payload adapter: the fields are Claude Code's, so `_ctx`, the write gate, the start block and the queue read a Codex event unchanged. The session's stem is the rollout file's (rollout-<time>-<id>), from transcript_path, as for Claude. Two additions in hook.py: `agent_of(payload)` tells Codex from Claude by the transcript path (rollout-… or /.codex/), recorded at SessionStart as the session's `agent` runtime key so the viewer can name it; and `_context` answers a Stop under Codex with `{"decision": "block", "reason": <the line>}`, because a Stop answered with hookSpecificOutput.additionalContext FAILS there — measured on 2026-09-18: "hook: Stop Failed", the turn ended, the model never saw it — while UserPromptSubmit, PostToolUse and SessionStart take additionalContext as Claude does. test_codex.py fires the measured payloads and checks all of it.

## Skills: .agents/skills in the project, loaded by description (to-do 41)
Measured 2026-09-18 with codex-cli 0.142.5: a `.agents/skills/probe-skill/SKILL.md` in the project (Claude's frontmatter: name, description) whose description said "whenever the user asks about the weather in Utrecht" was loaded by `codex exec "What is the weather like in Utrecht today?"` without being named — the reply began with the word the skill asked for. So the journal's ten skills install to `.agents/skills/<name>/` unchanged (install.codex_skills), beside `.claude/skills/` for Claude. Codex's own user-level folder is `$CODEX_HOME/skills` (~/.codex/skills); the project one is what the journal uses, so a project carries its skills with it.
