Codex hooks,
and the one that blocks.
Codex hooks are commands the Codex CLI runs at points in its loop, registered in ~/.codex/hooks.json and trusted by a hash in config.toml. PermissionRequest is the one that matters: Codex blocks on it, and it runs before the auto-reviewer decides for you.
A hooks.json with two entries
Events are PascalCase keys. Each entry carries a command, a timeout in seconds, an optional matcher, and an optional status message Codex shows while the hook is running.
{
"PermissionRequest": [
{
"hooks": [
{
"type": "command",
"command": "pushary-codex-hook",
"timeout": 600,
"statusMessage": "Waiting for your phone"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash|apply_patch|mcp__.*",
"hooks": [
{ "type": "command", "command": "pushary-codex-hook", "timeout": 600 }
]
}
]
}The events, and what each is for
Codex fires twelve. These are the ones worth wiring.
| Event | What it is for |
|---|---|
PermissionRequest | Fires when Codex escalates an action it will not run on its own. Codex blocks on it. Return allow or deny and the auto-reviewer is skipped; return nothing and the reviewer decides instead. |
PreToolUse | Runs before a tool call, matched by name. Inspect the tool and its input, then allow, block, or stand aside. This is the broad gate; PermissionRequest is the narrow one Codex raises itself. |
PostToolUse | Runs after a tool finishes, so you can log the command, lint the file that changed, or feed a follow-up note back to the model. |
Stop | Fires when the turn ends. Push a done alert, or return a block decision with a reason to keep the turn going. |
UserPromptSubmit | Fires when you submit a prompt, before the model sees it. Useful for injecting context or recording what was asked. |
SubagentStart / SubagentStop | Bracket a subagent's work, so parallel runs are visible without waiting on the whole turn. SessionStart, SessionEnd, PreCompact, PostCompact and Interrupt complete the set. |
A hook Codex does not trust is skipped
Codex hashes each hook and stores the result in config.toml under a snake_case key. Move the binary or edit the command and the hash drifts, so the entry is treated as Modified and silently skipped. This is the usual reason a hook that looks installed never runs.
[hooks.state]
permission_request = "sha256:fac756e3b892...b25ef0a"
pre_tool_use = "sha256:7ecd3e6d7503...9b9ea308"
stop = "sha256:57a22b4d044c...321e384e2"You go before the auto-reviewer
Left alone, Codex can hand an escalated action to its own reviewer, which refuses without asking and tells the model the action was rejected. A hook on PermissionRequest sees it first, and an answer from you skips the reviewer entirely.
allow
Settles the request. The command runs and the reviewer never sees it.
deny
Settles it the other way, with your reason handed back to the model.
nothing
Stands aside. The request falls through to the reviewer, exactly as it would without a hook.
A reviewer refusal can still be overturned from the Stop hook, by blocking the turn with a reason so the model re-issues the command and the retry's PermissionRequest is approved.
{
"decision": "block",
"reason": "user approved, rerun the command"
}How Pushary uses them
Pushary is a Codex hook you install with one command. It turns PermissionRequest into a phone approval and Stop into a done alert.
PermissionRequest waits for you
An escalated command runs the hook, which sends a push and holds the call until you approve or deny from your phone.
Stop pushes a done alert
When the turn ends, the Stop hook pushes a notification, so you can walk away and still know the moment it lands.
One command
npx @pushary/agent-hooks setupWrites the entries into ~/.codex/hooks.json and trusts them in config.toml. No manual JSON or TOML editing.
Put your Codex hooks to work.
Hold the escalated commands for your approval, and get a push when the turn ends.
Start your trialCodex hooks questions
Where do Codex hooks live?
Two files. The hook definitions go in ~/.codex/hooks.json, keyed by PascalCase event name, each entry carrying a command, a timeout and an optional matcher. Codex then records a trust hash per event in the [hooks.state] table of ~/.codex/config.toml, under snake_case keys like permission_request. Both have to agree.
Why does Codex say my hook is Modified and skip it?
Because the trust hash in config.toml no longer matches the hook. Codex hashes the command string together with the hooks.json path and stores it under [hooks.state]. Move the binary, change the command, or move hooks.json and the hash drifts, so Codex treats the entry as untrusted and silently skips it. Re-running the setup that wrote the hook re-trusts it.
Does a Codex hook actually block a command, or only observe it?
It blocks. Codex waits on PermissionRequest for an escalated action, so the command sits unrun until the hook returns. That is an enforced gate on your machine rather than a notification you might catch. The default hook timeout is 600 seconds, and a timeout you configure is honoured.
How does a hook interact with the Codex auto-reviewer?
PermissionRequest fires first. If the hook returns allow or deny, that settles it and the reviewer never runs. If the hook returns nothing, the request falls through to the reviewer, which decides on its own. A reviewer denial fires no hook at all, so the only record of it lives in a sibling guardian rollout rather than in your hook's input.
What permission_mode values does a Codex hook payload carry?
default, acceptEdits, plan, dontAsk and bypassPermissions. That is Claude Code's vocabulary minus auto, carried on Codex's own payload, so a hook written for one agent reads the same field on the other. Note that codex exec reports bypassPermissions whatever the approval policy and sandbox are set to, which is the correct headless answer.
What can a Codex Stop hook return?
Only continue, decision, reason, stopReason, suppressOutput and systemMessage. Anything else is dropped silently. Returning decision block with a reason keeps the turn going, which is how an approval can overturn an earlier refusal. A block still continues when stop_hook_active is set, so the hook has to guard against repeating itself.