Codex hooks: every event, the hooks.json format and working examples
By Jake Marsh, who builds Taskhold. Updated 2026-11-01. Facts about other tools checked 2026-11-01. 6 min read.
Short answer: Codex hooks are commands Codex runs at points in a session, such as before a tool call, before an approval prompt or when a turn stops. Put them in hooks.json or [hooks] tables in config.toml, in ~/.codex/ or a repo's .codex/. Each gets JSON on stdin. Approve new hooks with /hooks before they run. Exit code 2 blocks.
Hooks let you run your own code around the Codex agent: refuse a dangerous command, approve safe ones without a prompt, add context at session start, or send the agent back to work when a turn ends too early. This page lists every event, the file format, what each hook can return, and examples you can paste.
Everything here comes from OpenAI's Codex hooks documentation, checked on the date below. The same idea in other agents: Cursor hooks and Claude Code hooks. To watch several agents at once, try the live board demo.
Where hooks go
- User: ~/.codex/hooks.json, or [hooks] tables in ~/.codex/config.toml.
- Project: .codex/hooks.json or .codex/config.toml in the repo. Loaded only when the project's .codex/ layer is trusted.
- Plugins: hooks/hooks.json in the plugin, or a hooks entry in .codex-plugin/plugin.json.
- Managed: requirements.toml (managed_dir, or windows_managed_dir on Windows). allow_managed_hooks_only = true ignores every other source.
Layers add up rather than replace each other: every matching hook from every source runs. Hooks are on by default; [features] hooks = false in config.toml turns them off (codex_hooks is the old name for that key).
Before a non-managed hook runs, you review and trust it with /hooks. Trust is tied to the hook's hash, so editing a hook means approving it again. A hook that does nothing right after you add it usually hasn't been trusted yet.
The file format
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.codex/hooks/guard.sh", "timeout": 10, "statusMessage": "Checking command" }
]
}
]
}
}[[hooks.PreToolUse]]
matcher = "Bash"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "~/.codex/hooks/guard.sh"
timeout = 10- type: command, or mcp_tool (with server, tool and input) to call an MCP tool instead of a script.
- matcher: a regex on the tool name for the tool events (Bash, Edit, Write, MCP tool names), on manual or auto for compaction, on startup, resume, clear, compact or fork for SessionStart. Ignored for UserPromptSubmit, Stop and Interrupt.
- timeout: seconds, 600 by default. SessionEnd and Interrupt default to 1 and allow at most 3.
- commandWindows (command_windows in TOML): a different command on Windows.
- async: true runs the hook in the background. It can't block or change anything; up to eight run at once per session.
Every event
| Event | When it runs | What it can return |
|---|---|---|
| SessionStart | A session or subagent starts | additionalContext (plain stdout counts too) |
| UserPromptSubmit | Before your prompt is sent | decision block with a reason, additionalContext |
| PreToolUse | Before a shell, apply_patch, MCP or local tool call | permissionDecision deny with a reason, or allow with updatedInput |
| PermissionRequest | Before an approval prompt appears | decision.behavior allow or deny; any deny wins |
| PostToolUse | After a tool returns | decision block with a reason, additionalContext, continue false |
| PreCompact / PostCompact | Around context compaction | continue false stops it |
| SubagentStart / SubagentStop | A subagent starts or stops | context on start; decision block keeps it working |
| Stop | A turn ends | decision block: the reason becomes the next prompt |
| Interrupt | You interrupt a running turn | systemMessage only; it can't prevent the interrupt |
| SessionEnd | The session closes, or after 30 minutes idle | nothing (advisory) |
Every hook can also return continue, stopReason and systemMessage. Hook-specific fields go inside hookSpecificOutput.
Input and exit codes
Each hook gets one JSON object on stdin with session_id, transcript_path, cwd, hook_event_name and model; turn events add turn_id. Tool events add tool_name, tool_use_id and tool_input (and tool_response after the call). Stop adds last_assistant_message and stop_hook_active, which is true when the turn is already running because of an earlier Stop hook.
- Exit 0 with no output: carry on.
- Exit 2: block (PreToolUse, PostToolUse, UserPromptSubmit) or keep going (Stop, SubagentStop). Codex reads the reason from stderr.
- Stop, SubagentStop and Interrupt need JSON; plain text output is invalid for them.
- Hook commands run in the session's working directory, so repo hooks should build paths from the git root.
Example: refuse force pushes
#!/bin/bash
# ~/.codex/hooks/guard.sh (PreToolUse, matcher "Bash")
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'git push (-f|--force)'; then
echo "Force pushes are not allowed. Push normally or ask me." >&2
exit 2
fi
exit 0Example: keep going until the tests pass
#!/bin/bash
# .codex/hooks/tests-pass.sh (Stop)
active=$(jq -r '.stop_hook_active')
[ "$active" = "true" ] && { echo '{}'; exit 0; }
if ! bun test >/dev/null 2>&1; then
echo '{"decision":"block","reason":"The tests are failing. Run bun test and fix them before you stop."}'
else
echo '{}'
fiLimits
- PreToolUse and PostToolUse don't cover hosted tools such as web search, and writing to a running command's stdin doesn't trigger PreToolUse again.
- PreToolUse doesn't accept ask, continue false or stopReason: the hook run fails and the tool call goes ahead.
- PostToolUse can't undo what the tool already did.
- Output over about 2,500 tokens is saved to a file and replaced with a preview; additionalContextLimit changes that.
- prompt and agent hook types are read but skipped.
Hooks and a shared task board
Hooks see one Codex session at a time. Taskhold puts every agent's tasks on one live board: Codex reports steps and progress through the Taskhold MCP tools and asks its questions there, so you can answer from your phone. To copy each turn's last message onto the board, use the ready-made Codex notify recipe; connecting Codex is on the Codex setup page.
See how far along your coding agents are.
$12/month or $120/year after the trial. Card up front. Cancel before day 7 and you pay nothing. Every agent you run is included.
Questions
- Why isn't my Codex hook running?
- New and edited hooks must be approved with /hooks before they run, because trust is tied to each hook's hash. Project hooks also need the repo's .codex/ layer to be trusted, and [features] hooks = false turns all hooks off.
- Where do I put Codex hooks?
- ~/.codex/hooks.json or [hooks] tables in ~/.codex/config.toml apply everywhere. .codex/hooks.json or .codex/config.toml in a repo apply to that project once it is trusted. All matching hooks from every layer run.
- How do I auto-approve safe commands in Codex?
- Use a PermissionRequest hook. It runs before the approval prompt and can return decision.behavior allow or deny. If any hook denies, the request is denied; if none decides, you see the normal prompt.
- What's the difference between notify and hooks?
- notify in config.toml runs one program after each turn and passes it the event, including the last message, as one JSON argument. Hooks cover many more events, can block or change what the agent does, and read JSON on stdin.
Sources
- Codex docs: hooks, checked 2026-11-01
Related
Updated 2026-11-01. Something wrong or out of date? Email hello@greatwork.company.