Skip to content

Gate a risky move with a hook

There’s one move in budgetcli you treat as sacred: Codex must never write to your real ledger database. It can read it, reason about it, generate a migration for you to review - but the live table where your actual account balances are counted is off-limits to automated writes. An accidental UPDATE there isn’t a bug you find in code review; it’s wrong numbers in your own money. You could put this in AGENTS.md and ask the agent to honor it, and most of the time it would. But “most of the time” is precisely the wrong guarantee for the one thing that must never slip. Use a hook as one deterministic check, and back it with database permissions, credentials, and sandboxing for the hard boundary.

That’s a hook: a shell command Codex runs at a matching lifecycle point. It is deterministic and independent of the model’s reasoning, but it is not a complete security boundary: hooks can require trust review, and specialized tool paths may not use the normal hook path. Use hooks as defense-in-depth alongside sandboxing, CI, database permissions, and network controls. The full event list and payload detail are on the Codex hooks docs.

The way a hook talks back is equally simple, and it’s the second half of the concept: the handler’s documented decision contract determines whether the action proceeds or is blocked, and diagnostic output can be returned to the agent. Follow the current hook contract for the Codex release you deploy; do not treat an illustrative exit-code detail as a substitute for the documented enforcement path.

Hooks attach to lifecycle events. Codex documents a family of them - SessionStart when a session opens, UserPromptSubmit when you send a message, PreToolUse before a tool runs, PostToolUse after, PreCompact/PostCompact around compaction, PermissionRequest when the agent asks to do something gated, and Stop when the session ends. Picking the right one is most of the design.

For our wall we want PreToolUse: it fires before a tool call runs, which is the only moment you can stop a write from ever happening. We’ll inspect the command Codex is about to run and refuse it if it touches the live ledger.

Hooks are configured next to your active config layers - a hooks.json file at ~/.codex/ or in the repo’s .codex/, or inline as [hooks] tables in config.toml. You name the event, a matcher that narrows which tool calls trigger the hook, and the command to run.

.codex/hooks.json:

{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./.codex/hooks/protect-ledger.sh" }
]
}
]
}

The matcher narrows this to shell tool calls - psql, a migration runner, anything that could touch the database - so the hook isn’t woken for every file read.

Codex hands a hook a JSON payload describing the call and the session - fields like session_id, transcript_path, cwd, hook_event_name, model, and permission_mode, with a turn_id on turn-scoped hooks. The script below greps the command string out of that payload; the tool_input.command path is the illustrative one, so verify it against the payload schema in the docs.

.codex/hooks/protect-ledger.sh
#!/bin/bash
input=$(cat)
# Field path below is illustrative - verify against the hooks payload schema.
command=$(echo "$input" | jq -r '.tool_input.command')
# Refuse any write that targets the live ledger database.
if echo "$command" | grep -Eqi 'ledger_prod|UPDATE .*ledger|INSERT .*ledger|DELETE .*ledger'; then
echo "Blocked: writes to the production ledger are not allowed from an agent session. \
Generate a migration for review instead." >&2
exit 2
fi
exit 0

Everything in that script is plain shell - nothing here is AI, which is the whole point. The exit 2 and the stderr message are the exit-code contract from the top of the lesson, doing their job.

Codex finishes some balance-reconciliation work and reaches for the database:

> apply the corrected balances to the ledger
● Bash psql budgetcli -c "UPDATE ledger_prod SET balance_cents=... "
⊘ Blocked by PreToolUse hook (protect-ledger.sh):
Blocked: writes to the production ledger are not allowed from an
agent session. Generate a migration for review instead.
Understood - I can't write the live ledger directly. I'll emit the
corrections as a reviewed migration instead.
● Write migrations/0007_reconcile_balances.sql
-- review and apply this yourself

Watch what the agent did with the rejection. It didn’t get stuck - it read the diagnostic output the hook provided, understood why, and rerouted to the safe path: a migration you apply by hand. The gate didn’t just stop a bad action; it steered Codex toward the right one. In a headless run, it can provide the same guardrail when the invocation reaches the supported, trusted hook path.

The other shape: run the tests after the money math changes

Section titled “The other shape: run the tests after the money math changes”

PreToolUse blocks; its sibling PostToolUse observes after a tool runs and can’t block - perfect for “do this every time the agent edits a file.” The money math in budgetcli is the part you least trust to a silent edit, so wire a PostToolUse hook on file edits to run the test suite:

.codex/hooks.json:

{
"PostToolUse": [
{
"matcher": "apply_patch",
"hooks": [
{ "type": "command", "command": "pytest tests/test_money.py -q" }
]
}
]
}

Now every matching edit Codex makes can be followed by the money tests, whether or not the agent thought to run them. A regression in the cents math surfaces close to the edit instead of three edits later; keep the test suite or CI as the authoritative gate.

You’ve now built something categorically different from the MCP servers earlier in this chapter. A server widened what Codex can do; a hook overrules it. That difference is sharp enough to name precisely - because knowing when to reach for a hook instead of a rule or an approval setting is its own skill, and it’s the next lesson.