New: watch your coding agents work, step by step, on a live board. Try the demo, no account

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" }
        ]
      }
    ]
  }
}
Each event takes a list of matcher groups, and each group a list of hooks.
[[hooks.PreToolUse]]
matcher = "Bash"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "~/.codex/hooks/guard.sh"
timeout = 10
The same hook as a table in config.toml. If one layer has both forms, Codex merges them and warns at startup.
  • 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

Every event. Every hook can also return continue, stopReason and systemMessage. Hook-specific fields go inside hookSpecificOutput.
EventWhen it runsWhat it can return
SessionStartA session or subagent startsadditionalContext (plain stdout counts too)
UserPromptSubmitBefore your prompt is sentdecision block with a reason, additionalContext
PreToolUseBefore a shell, apply_patch, MCP or local tool callpermissionDecision deny with a reason, or allow with updatedInput
PermissionRequestBefore an approval prompt appearsdecision.behavior allow or deny; any deny wins
PostToolUseAfter a tool returnsdecision block with a reason, additionalContext, continue false
PreCompact / PostCompactAround context compactioncontinue false stops it
SubagentStart / SubagentStopA subagent starts or stopscontext on start; decision block keeps it working
StopA turn endsdecision block: the reason becomes the next prompt
InterruptYou interrupt a running turnsystemMessage only; it can't prevent the interrupt
SessionEndThe session closes, or after 30 minutes idlenothing (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 0
Needs jq. chmod +x the script, then approve it with /hooks.

Example: 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 '{}'
fi
Checking stop_hook_active sends the agent back once, not forever.

Limits

  • 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.

Share this page:XLinkedInBlueskyHacker News

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

Related

Updated 2026-11-01. Something wrong or out of date? Email hello@greatwork.company.