Hooks embed command scripts directly into the agentic loop: they react to lifecycle events and can allow, modify or block an action. Codex reads hooks.json and inline [hooks] sections next to the active config layers, and matching hooks from different sources run together. Like the rest of the executable configuration, project hooks take effect only in a trusted project - this is the first line of defense from foreign code that arrived with the repository.
It helps to see a hook definition in full once. Below is hooks.json with the PreToolUse event, a matcher by the tool name "^Bash$", a handler command and a timeout. The matcher matters: it sets which exactly actions the hook triggers on - here on attempts to run Bash. It is the bundle of the event and the matcher that makes a hook a pointed tool reacting to a specific class of actions rather than to everything.
A hook's trust is tied to the hash of its definition, and this is a key security mechanism. A non-managed hook requires a review of the exact definition; on a change of the command its status returns to "needs review". That is, a changed hook does not start executing silently - it becomes untrusted again until you have explicitly confirmed its new content. This is protection from a quiet substitution: an edit of a hook by someone else does not pass unnoticed past your attention.
You manage hooks' trust and state through a separate command. /hooks shows the source of each hook, its trust and whether it is enabled. This removes the uncertainty: instead of assuming which hooks are in play and who approved them, you see a list with sources and statuses. Separately there is the --dangerously-bypass-hook-trust flag, but it has a narrow place of use - only automation where the source is verified before the run, not an interactive bypass of an inconvenient check.
A hook's handler is arranged by a simple protocol that must be followed. It gets JSON on stdin and reports the decision through the exit-code and status protocol described in the official guide. It helps to see this in a bundle with the definition once. The handler is an ordinary program with the rights of the Codex process, so it is written as code to answer for: with a clear input, an explicit decision and predictable behavior, not as a one-off script at random.
Three rules of a safe handler matter more than the rest. Do not print secrets - a hook's output may get into the context or logs. Do not make a hook silently destructive - an action with consequences must be explicit and expected, not hidden inside a check. And keep the timeout shorter than the user's patience: a hook that hangs longer than a person is ready to wait turns protection into irritation. The failure mode meanwhile must be clear, not a mysterious hang.
The point of hooks is a deterministic check where the model's judgment cannot be relied on. A mandatory lint after an edit, a ban on a dangerous command, a policy check before an action - all of this a hook will execute the same way always, unlike the model, which may treat one situation differently. But precisely because a hook is executable code with the process's rights, its trust, timeout and failure mode are designed as carefully as the check itself.
The typical failures around hooks are predictable. Writing a too-broad matcher and triggering on the wrong thing. Forgetting that a change of the command returns the hook to "needs review" and being surprised it does not work. Applying --dangerously-bypass-hook-trust in interactive work instead of narrow automation. And writing a handler that prints secrets, is silently destructive or hangs longer than patience. Set precise matchers, review by hash, write the handler as code and keep the timeout and failure mode clear.
{
"description": "Repository policy checks",
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{ "type": "command",
"command": "node .codex/hooks/check-command.mjs",
"timeout": 10 }
]
}
]
}
}
// trust is tied to the hash; a command change -> "needs review"; /hooks shows source/trust/enable