Codex hooks let you run your own script at fixed points in the agentic loop: when a task starts, when a prompt is submitted, before and after a tool call, when a turn stops. They are configured in .codex/hooks.json for a project, ~/.codex/hooks.json for your own defaults, or as an inline [hooks] table in either config.toml. That is the whole idea. The rest of this page is the part the reference has to state carefully and a first-time reader usually learns the slow way: the exact three-level shape, the trust gate, which events exist, and what a hook still cannot do. We wire seven of these events in a shipped product, so the worked example below is a real file, not a sketch.
What a Codex hook actually is
The config is three levels, and mixing up the middle one is the most common reason a hook never fires. From the outside in: an event (PreToolUse, Stop, and the rest), then a matcher group that decides when that event counts, then one or more handlers that run when the group matches.
A handler is an object with type: "command" and a command string. Two optional keys matter in practice. timeout is in seconds, and if you omit it Codex uses 600, so a hook that hangs can hold a turn for ten minutes unless you say otherwise. statusMessage is the line Codex shows while your hook runs, which is the difference between a user seeing a named check and a user seeing an unexplained pause.
PreToolUse, PostToolUse, PreCompact, PostCompact, PermissionRequest, UserPromptSubmit, Stop, SubagentStop, SessionStart, SubagentStart. Named points in the loop.matcher regex and a hooks array. On tool events the matcher tests the tool name, so "Bash" scopes a check to shell calls. Omit matcher and the group always matches, which is what you want on UserPromptSubmit.{ type: "command", command: "..." }, plus optional timeout (seconds, default 600), statusMessage, and commandWindows (a Windows-only override, command_windows in TOML). Only type: "command" runs today: prompt and agent handlers are parsed and then skipped, and async is parsed but not supported yet.The trust gate, or why your first hook does nothing
This is the one that costs people an hour. A non-managed command hook does not run until you review and trust it. You write a perfectly good hooks.json, restart, watch nothing happen, and start debugging your script. The script is fine. Codex is waiting on you. Run /hooks in the CLI to inspect the hook sources, review what is new, and trust it. If anything needs review at startup, Codex prints a warning pointing you there.
Then the detail behind the detail: trust is recorded against the hook's current hash. Change the command string and it is a new hook, marked for review again, skipped until you trust it again. This has a direct consequence for any installer, ours included. Our wiring writes an absolute path to your Node binary into every command string, so the day you upgrade Node, every kitstarter hook's hash changes and Codex quietly puts them all back in the review queue. That is Codex working exactly as designed, and it is why kitstarter doctor checks that the wired paths still exist rather than assuming a green install stays green.
Two more rules worth knowing before you debug the wrong thing. A project's hooks load only when its `.codex/` layer is trusted, so hooks in a repo you have not trusted are not skipped by your config, they are skipped by the trust model. And for one-off automation that vets hook sources some other way, --dangerously-bypass-hook-trust runs enabled hooks without persisted trust for that invocation. The name is the review.
Where Codex looks, and why two layers can double-fire
Codex discovers hooks next to each active config layer, in four places that matter: ~/.codex/hooks.json, ~/.codex/config.toml, the project's .codex/hooks.json, and the project's .codex/config.toml. Plugins can bundle their own through a manifest or a default hooks/hooks.json.
Here is the behavior that surprises people, and it is the opposite of how most config systems work. A higher-precedence layer does not replace a lower one. All matching hooks from every source run. So a hook installed globally and the same hook installed in the project do not resolve to one winner: you get both, twice, every time. If a check of yours is suddenly running in duplicate, that is the first place to look. And if one layer contains both a hooks.json and an inline [hooks] table, Codex merges them and warns at startup, so prefer one representation per layer.
One more thing about how they run: multiple matching command hooks for the same event are launched concurrently, and one hook cannot stop another from starting. Hooks are not a pipeline, and they are not ordered. Do not write two that depend on each other.
A real setup: seven events, eleven handlers
Reference docs show one hook. Here is a whole file, from a product people pay for, so you can see what a real config looks like when it has to survive other people's machines. kitstarter wires seven events and eleven handlers into .codex/hooks.json (or ~/.codex/hooks.json on a global install). The source is hooks/lib/codex-wiring.mjs; every command points at one adapter, hooks/codex-hook.mjs, with the target script as its argument.
startup|resume|clear|compact. One prints the project roadmap and where you left off, one is the tutor. Their output is returned as additionalContext, so it lands in the task rather than on your screen.manual|auto. The tutor teaches context hygiene at the exact moment the task is about to compact, which is the moment it is worth knowing.safety-guard.mjs on matcher Bash warns before a destructive command. clarity.mjs on matcher apply_patch|Edit|Write fires before a file edit. Note the first alternative: apply_patch is Codex's own edit tool, and a config that only matches Edit|Write silently never fires here.apply_patch|Edit|Write. One scans the file that was just written for anti-slop tells, one decides whether the change earned a short learning card. Both are advisory and neither blocks.Two decisions in that file are worth stealing regardless of what you are building. First, it merges, it does not overwrite. Every handler we write carries a marker; wiring filters out only the groups whose command contains that marker, then appends ours. Your own hooks in the same file are left exactly where they were, and uninstalling removes our handlers and nothing else. An installer that writes hooks.json wholesale is an installer that eats a config you spent an afternoon on. Second, it refuses to guess at a file it cannot parse. If your hooks.json is not valid JSON, wiring returns false and the installer prints a warning instead of clobbering it.
The timeout is set to 15 seconds on every handler, against a default of 600. A coaching hook has no business holding your turn for ten minutes; if it cannot answer in fifteen seconds, the right answer is silence. Which is the same instinct as the last section.

What a Codex hook cannot do yet, said plainly
This is the part a vendor page leaves out, and it is the part that decides whether a hook can enforce anything. Codex's `PreToolUse` hook is warning-only today. It can say something before a tool call. It cannot return a decision that denies the call. Claude Code's equivalent can: a hook there returns permissionDecision: "deny" and the edit does not happen.
That gap is load-bearing for us, so we will show our work rather than paper over it. The kit's clarity engine is meant to lock file edits until you have confirmed the goal. On Claude Code it does exactly that. On Codex it cannot, so hooks/codex-hook.mjs translates the deny into a plain warning and the instruction injected at UserPromptSubmit carries the rule instead: finish the questions, get an explicit yes, do not write files before that. The adapter does the same for Stop. Codex Stop hooks do support continue: false, but they do not expose Claude Code's loop-prevention signal, so a strict block gets translated to a warning rather than risking a task trapped in a stop loop. Instruction where Codex allows instruction, enforcement only where enforcement is real. A page telling you a hook can block an edit in Codex today is describing Claude Code and changing the file names.
Worth watching: there is a PermissionRequest event, distinct from PreToolUse, that fires around the approval itself. That is the surface where real gating would live, and it is where this gap most plausibly closes. Codex ships fast enough that anything in this section is worth re-reading against the docs before you rely on it, including this sentence.
The honest summary: Codex hooks are a good extension point with one real limit. They fire reliably at every point that matters, the config is simple once you see the three levels, and the trust gate is a feature that reads as a bug exactly once. What they cannot do yet is stop a tool call, which means today a Codex hook is best at **noticing, injecting context, and telling the truth at the end of a turn** rather than at policing. Build for that and they will not disappoint you. If you would rather not wire eleven handlers by hand, kitstarter ships this exact config for Codex and Claude Code, and kitstarter doctor tells you which ones are actually live. If you are still choosing, the honest Codex vs Claude Code comparison is next door.
Common questions
What are Codex hooks? Hooks are your own scripts, run by Codex at fixed points in the agentic loop: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, PostCompact, PermissionRequest, Stop, SubagentStart, and SubagentStop. You configure them in .codex/hooks.json for a project, ~/.codex/hooks.json for your defaults, or an inline [hooks] table in config.toml. Each entry has three levels: the event, a matcher group, and one or more command handlers.
Why is my Codex hook not running? Most often because you have not trusted it. A non-managed command hook has to be reviewed and trusted before Codex will run it, and trust is recorded against the hook's exact hash, so editing the command marks it for review again. Run /hooks in the CLI to review and trust it. Also check that the project's .codex/ layer is trusted, since project hooks only load in a trusted project, and that your matcher tests the right tool name.
Where does Codex look for hooks? Next to each active config layer. The four locations that matter are ~/.codex/hooks.json, ~/.codex/config.toml, the project's .codex/hooks.json, and the project's .codex/config.toml. Enabled plugins can bundle hooks too. All matching hooks from every source run: a higher-precedence layer does not replace a lower one, so the same hook installed globally and per project will fire twice.
Can a Codex hook block a tool call? Not today. The PreToolUse hook is warning-only: it can print a message before a tool call but cannot return a decision that denies it. Claude Code's PreToolUse hook can deny. If you need a check that stops an edit in Codex, put the rule in the task instruction at UserPromptSubmit and use the hook to warn, rather than expecting the hook to enforce it.
What is the timeout on a Codex hook? The timeout key is in seconds and defaults to 600 if you omit it, so a hanging hook can hold a turn for ten minutes. Set it explicitly. For anything advisory, a short timeout is better than a long one: kitstarter sets 15 seconds on all eleven of its Codex handlers, on the principle that a coaching hook that cannot answer quickly should stay quiet.
Eleven Codex hooks, wired for you
kitstarter installs the whole config into .codex/hooks.json, merges with the hooks you already have instead of overwriting them, and ships a doctor that tells you which handlers are live. One install covers Codex and Claude Code.
