Who this is for
This is for readers who want to add their own rules to Codex: blocking risky Bash commands, scanning prompts for pasted API keys, logging sessions, or running a check when a turn stops. By the end you can configure a PreToolUse hook, trust it, and know when it runs.
What hooks are
Hooks are Codex's extension mechanism. They let you run scripts or MCP tools during the agent loop.[1] The docs give examples such as sending the chat to a custom logging or analytics engine, scanning prompts to block accidentally pasted API keys, summarizing chats into persistent memories, and running a validation check when a turn stops.[1]
Hooks come from every matching source, and all of them run. When several matching command hooks share an event, they start concurrently, so one hook cannot stop another from starting.[1]
When hooks run
Hook events fire at different points in a session:[1]
- During a turn:
PreToolUse,PermissionRequest,PostToolUse,PreCompact,PostCompact,UserPromptSubmit,SubagentStopandStop. - For each model request attempt:
PreModelRequestandPostModelRequest. - When you interrupt an active turn:
Interrupt. - When a session or subagent starts:
SessionStartandSubagentStart. - When the main thread ends:
SessionEnd.
Where to put them
Codex finds hooks next to its active config layers, either as a hooks.json file or as inline [hooks] tables in config.toml.[1] The four most useful places are ~/.codex/hooks.json, ~/.codex/config.toml, <repo>/.codex/hooks.json and <repo>/.codex/config.toml.[1] If one layer has both forms, Codex merges them and warns at startup, so keep one form per layer.[1]
A hook has three levels: an event, a matcher group that decides when the event matches, and one or more handlers.[1] Only command and mcp_tool handlers run. prompt and agent handlers are parsed but skipped.[1] Here is an inline config.toml version with a SessionStart hook and a PreToolUse hook that checks Bash commands:
[[hooks.SessionStart]]
matcher = "^compact$"
[[hooks.SessionStart.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/session_start.py"'
additionalContextLimit = 5000
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py"'
timeout = 30
statusMessage = "Checking Bash command"For a repository-local hook, resolve the script path from the git root instead of a relative path such as .codex/hooks/..., because Codex may be started from a subdirectory.[1] Commands run with the session's working directory as cwd.[1] A timeout is in seconds. Most hooks default to 600 seconds, while SessionEnd and Interrupt default to 1 second and can go up to 3 seconds.[1]
Trust comes first
Non-managed hooks do not run until you trust them. Codex stores trust against each hook's hash, so a new or edited hook is marked for review and skipped until you approve it.[1] Run /hooks in the CLI to see hook sources, review changes, trust hooks or disable individual ones.[1] The --dangerously-bypass-hook-trust flag skips the persisted trust check for a single run. The docs reserve it for one-off automation that already vets hook sources outside Codex.[1]
Turn hooks off
Hooks are on by default. To disable them, add this to config.toml:
[features]
hooks = falseThe key is hooks. codex_hooks still works, but it is a deprecated alias.[1]
Blocking a tool call
PreToolUse can intercept Bash commands, file edits made through apply_patch, MCP tool calls and other local function tools. Hosted tools such as WebSearch are not covered.[1] To deny a call, print JSON like this:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook."
}
}Exit code 2 with the reason on stderr also blocks the call.[1] A hook can instead rewrite a call by returning permissionDecision: "allow" with updatedInput. For Bash and apply_patch, that object must include a string command field.[1] The docs call tool hooks a useful guardrail, not a complete enforcement boundary.[1]
PermissionRequest runs when Codex is about to ask for approval, such as a shell escalation. It can allow or deny the request. If several hooks decide, any deny wins.[1]
Keeping a turn going and running in the background
A Stop hook can keep Codex working. Returning decision: "block" with a reason makes Codex continue, using that reason as a new user prompt, rather than rejecting the turn.[1] Set "async": true on a command handler to run it in the background. Up to eight background hooks run at once per session, but they cannot block or rewrite the action that triggered them, so use synchronous hooks for policy decisions.[1]
Output size
By default Codex limits each model-visible hook output to about 2,500 tokens. Larger output is saved to a file under <temp_dir>/hook_outputs/ and replaced with a preview.[1]
Matchers and managed hooks
matcher is a regex. "*", an empty string, or no matcher matches every occurrence of a supported event. UserPromptSubmit and Stop ignore it.[1] In enterprise setups, allow_managed_hooks_only = true in requirements.toml skips hooks from user, project, session and plugin sources, but keeps managed hooks.[1]
Next steps
Start with one PreToolUse hook for Bash, confirm it with /hooks, then add more events. The full event schemas are in the official Hooks page.[1]
Sources
- 1HooksOpenAI Codex docs
Cite this page: Codex hooks: running scripts and MCP tools in the agent loop. TellWorld, https://www.tellworld.net/practice/practice-codex-hooks. Updated 2026-11-14