Codex hooks are commands or MCP tool calls that Codex runs at fixed points in its agent loop, such as before a shell command, after a file edit, or when a turn ends. You define them in hooks.json or in [hooks] tables in config.toml, and Codex runs every matching hook after you review and trust it.
This guide covers where hooks live, the events you can hook into, how to block or rewrite a tool call, and the limits to know about. Everything here comes from OpenAI's Codex hooks documentation, read on 24 October 2026.
What can you do with Codex hooks?
OpenAI's docs list these example uses:
- Send the chat to your own logging or analytics system.
- Scan prompts so nobody pastes an API key by accident.
- Summarize chats into persistent memories.
- Run a validation check when a turn stops, so standards are enforced.
- Change prompting when Codex works in a certain directory.
In practice, hooks are where you put rules that should run every time, not only when the model remembers them. An instruction in AGENTS.md asks Codex to run the linter; a Stop hook can make the turn continue until it does.
Where does Codex look for hooks?
Codex discovers hooks next to its active config layers, in two forms: a hooks.json file, or inline [hooks] tables inside config.toml. The four locations you will use most:
| Location | Scope |
|---|---|
~/.codex/hooks.json |
You, in every project |
~/.codex/config.toml |
You, in every project |
<repo>/.codex/hooks.json |
Everyone working in this repo |
<repo>/.codex/config.toml |
Everyone working in this repo |
A few loading rules matter:
- Hooks add up. If several sources define hooks, Codex loads all matching ones. A higher-precedence layer does not replace hooks from a lower one.
- One format per layer. If a layer has both
hooks.jsonand inline[hooks], Codex merges them and warns at startup. - Projects must be trusted. Project-local hooks load only when the project's
.codex/layer is trusted. In an untrusted project, your user and system hooks still load. - Plugins can bring hooks. An enabled plugin can bundle hooks in
hooks/hooks.jsonor point to them from.codex-plugin/plugin.json. See the Codex plugins guide.
The Codex config guide explains how user and project layers combine.
Which events can Codex hooks run on?
| When | Events |
|---|---|
| During a turn | PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, UserPromptSubmit, SubagentStop, Stop |
| When you interrupt an active turn | Interrupt (not for subagents) |
| When a session or subagent starts | SessionStart, SubagentStart |
| When the main thread ends | SessionEnd (not for subagents) |
What each one is good for:
- SessionStart adds context when a session starts, resumes, clears, compacts or forks. Plain text on stdout becomes extra developer context.
- UserPromptSubmit sees each prompt before it is sent and can block it, which is how you catch pasted secrets.
- PreToolUse runs before shell commands,
apply_patchfile edits, MCP tool calls and most other local tools. It can deny or rewrite the call. - PermissionRequest runs only when Codex is about to ask you for approval. It can allow, deny, or let the normal prompt appear.
- PostToolUse runs after a tool returns, including Bash commands that fail. It cannot undo the action, but it can replace the result with feedback for the model.
- PreCompact and PostCompact run around context compaction.
- SubagentStart and SubagentStop run for Codex subagents.
- Stop runs when a turn is about to end and can make Codex keep going.
- Interrupt and SessionEnd are for logging and cleanup. They cannot change what happens.
What does a hooks.json file look like?
Hooks have three levels: an event, a matcher group that decides when the event matches, and one or more handlers. This .codex/hooks.json runs a policy check before every shell command:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"",
"statusMessage": "Checking Bash command",
"timeout": 30
}
]
}
]
}
}
The same hook as inline TOML in config.toml:
[[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"
Handler fields worth knowing:
type:commandormcp_tool. Thepromptandagenttypes are parsed but skipped today.timeout: in seconds. The default is 600 for most events.SessionEndandInterruptdefault to 1 second and allow at most 3.statusMessage: text shown while the hook runs.async: settrueto run a command hook in the background.commandWindows: a Windows-only override ofcommand.
Commands run with the session's working directory. Because Codex may start in a subfolder, the docs recommend resolving repo hooks from the git root, as above, instead of a relative path like .codex/hooks/....
How do matchers work?
matcher is a regex. Use "*", "" or leave it out to match everything. What it filters depends on the event:
| Event | Matcher filters |
|---|---|
PreToolUse, PostToolUse, PermissionRequest |
Tool name, such as Bash, apply_patch or mcp__filesystem__read_file |
SessionStart |
startup, resume, clear, compact or fork |
PreCompact, PostCompact |
manual or auto |
SubagentStart, SubagentStop |
The subagent type |
UserPromptSubmit, Stop, Interrupt |
Not supported, any matcher is ignored |
For file edits, apply_patch also matches Edit and Write, so Edit|Write catches every patch. MCP tools use the mcp__<server>__<tool> form, and mcp__filesystem__.* matches a whole server. Hosted tools such as web search do not go through the local hook path, so hooks cannot see them.
How do you block or rewrite a tool call?
Every command hook gets one JSON object on stdin. Shared fields include session_id, cwd, hook_event_name, model and transcript_path, and tool events add tool_name and tool_input. For Bash, the command is in tool_input.command.
Here is an example policy script for the hook above, written for this guide rather than taken from OpenAI's docs. It denies any command containing rm -rf, using the deny shape the docs document:
#!/usr/bin/env python3
import json, sys
event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")
if "rm -rf" in command:
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook."
}
}))
# exit 0 with no output lets the call continue
Other responses PreToolUse supports:
- Exit code 2 with the reason on stderr also blocks the call.
- Add context without blocking by returning
hookSpecificOutput.additionalContext. - Rewrite the call by returning
permissionDecision: "allow"withupdatedInput. For Bash andapply_patch,updatedInputmust include a stringcommand.
permissionDecision: "ask" is parsed but not supported yet: Codex marks the hook as failed and lets the tool call run. For PermissionRequest, return decision.behavior set to allow or deny; if several hooks answer, any deny wins.
To keep Codex working, a Stop hook returns {"decision": "block", "reason": "Run one more pass over the failing tests."}. Codex continues the turn and uses the reason as a new prompt. The input includes stop_hook_active, so your script can avoid sending Codex round in circles. Unlike SessionStart, Stop expects JSON on stdout when it exits 0; plain text is invalid.
How do you trust, review and turn off hooks?
Before a non-managed hook runs, Codex asks you to review and trust its exact definition. Trust is stored against a hash of the hook, so a new or edited hook is skipped until you approve it again. Type /hooks in the Codex CLI to see hook sources, review changes, trust hooks or disable individual ones. If hooks need review, Codex prints a startup warning pointing you to /hooks.
For scripted runs that already vet hook sources, --dangerously-bypass-hook-trust runs enabled hooks without stored trust for that one invocation.
To turn hooks off entirely:
[features]
hooks = false
Admins can force the same setting in requirements.toml, define managed hooks that run scripts from a managed_dir (their own tooling installs the scripts), and set allow_managed_hooks_only = true to ignore user, project, session and plugin hooks. Managed hooks are trusted by policy and cannot be disabled from /hooks.
What are the limits of Codex hooks?
- Not a security boundary. The docs say to "Treat tool hooks as a useful guardrail, not a complete enforcement boundary." Some tool paths skip hooks, and hosted tools are not covered. Pair hooks with the sandbox and approvals.
- Concurrent, not chained. Matching command hooks for the same event start at the same time, so one hook cannot stop another from starting.
- Background hooks can't decide. An
asynchook cannot block, approve or rewrite anything. Codex runs up to eight at once per session (more wait their turn) and cancels unfinished ones when the session ends. - Output is capped. Model-visible hook output is limited to roughly 2,500 tokens by default. Longer output is saved to a temp file and the model gets a preview. Raise it per handler with
additionalContextLimit, and keep secrets out of hook output. - MCP tool hooks use existing connections. A
mcp_toolhandler calls a tool on an MCP server that is already connected; it never starts one. Errors and missing servers do not block the operation.
For the exact wire format, OpenAI publishes generated schemas in the openai/codex repository. If you also use Claude Code or Cursor, compare the Claude Code hooks guide and the Cursor hooks guide. Codex sets CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA for plugin hooks "for compatibility with existing plugin hooks."
Where should the rest of your Codex setup live?
Hooks enforce rules; skills hold the know-how Codex follows. Kipt keeps your prompts and Agent Skills as Markdown in a private GitHub repo you own and installs every skill into Codex or Claude Code with npx skills add https://github.com/<you>/<repo>.git --all -g. The free SKILL.md generator turns a prompt you already use into a skill.