Hooks let Codex run user-defined handlers (command / prompt / agent) when specific lifecycle
events fire. They are useful for policy checks (for example, blocking a risky tool call),
for harness-style loops (for example, blocking stop until a condition is met), and for
injecting additional context into the next model turn.
This document describes:
- Where hooks can be configured
- How hooks execute (parallelism, deduplication, blocking)
- The hook payload contract (stdin JSON)
- The hook output contract (stdout JSON)
Command hooks can be configured in config.toml under the [hooks] table.
- User config:
~/.codex/config.toml - Project config:
./.codex/config.toml(searched upward to the project root)
If a project directory is untrusted, Codex will still discover ./.codex/config.toml but load
it as a disabled layer. To trust a project, add an entry in your user config:
[projects."/absolute/path/to/project"]
trust_level = "trusted"Skills may define hook handlers in the YAML frontmatter of SKILL.md under hooks:.
These hooks are installed as scoped hooks for the duration of the turn in which the skill is active, and removed automatically at the end of the turn.
This is the closest analogue to Claude Code’s “hooks in skills”.
When an event fires:
- All matching hooks run in parallel.
- Identical handlers are deduplicated automatically (so the same handler is only run once for that event fire, even if it appears in multiple hook sources or matcher groups).
- Synchronous hooks are awaited before the triggering action proceeds.
once = trueruns that handler at most once per Codex session.timeoutapplies per handler (seconds). If unset:- Command hooks have no timeout.
- Prompt hooks default to 30s.
- Agent hooks default to 60s.
Only command hooks support async = true.
An async hook:
- Runs in the background and never blocks the triggering action.
- Cannot reliably block, rewrite inputs, or auto-approve permissions (the action already continued).
- Delivers its
additionalContext/systemMessageoutput on the next turn (if the session is idle, it waits until the next user interaction). - Does not deduplicate across repeated firings of the same hook (each fire spawns a new process).
Command hooks run a shell command. Codex:
- Writes a single JSON object (the hook payload) to the process
stdin. - Reads
stdoutand attempts to parse a JSON object (either the full output or the first parseable JSON line).
Exit codes:
0: success;stdoutJSON (if any) is applied.2: blocks blockable events;stdoutis ignored andstderrbecomes the block reason.- Any other non-zero: treated as a non-blocking error and execution continues.
Prompt hooks send a single-turn evaluation prompt to the model. The prompt can reference the hook
input JSON via $ARGUMENTS:
- If the prompt contains
$ARGUMENTS, Codex replaces it with the serialized input JSON. - Otherwise Codex appends a
$ARGUMENTS:section automatically.
The model must return JSON only:
{ "ok": true }or:
{ "ok": false, "reason": "..." }ok: false is treated as a blocking decision (for blockable events).
If a prompt hook times out, the request fails, or the response is not valid JSON, Codex records the error and continues (non-blocking).
Agent hooks spawn a verifier subagent that can use tools (Read/Grep/Glob/etc) and must return a
final JSON-only message with the same {ok, reason} shape as prompt hooks.
If an agent hook times out, fails to spawn, or returns invalid JSON, Codex records the error and continues (non-blocking).
Matchers are optional filters. A matcher is only applied for events that support matching.
matcher is a Rust regex pattern that matches an event-specific string:
- Tool events (
pre_tool_use,permission_request,post_tool_use,post_tool_use_failure):tool_name session_start:sourcesession_end:reasonnotification:notification_typesubagent_start/subagent_stop:agent_typepre_compact:triggerconfig_change:source
Special case: matcher = "*" means “match all” (equivalent to omitting it).
For tool events, you may also use:
tool_name(exact match)tool_name_regex(Rust regex)
These events ignore matchers:
user_prompt_submitstopteammate_idletask_completedworktree_create,worktree_remove
All events share these top-level fields:
session_id(string)transcript_path(string|null)cwd(string)permission_mode(string; for exampleon-request,on-failure,untrusted,never)hook_event_name(string; PascalCase)
Event-specific fields are flattened at the top level based on hook_event_name.
Event payload fields:
SessionStart:source,model,agent_typeSessionEnd:reasonUserPromptSubmit:promptPreToolUse:tool_name,tool_input,tool_use_idPermissionRequest:tool_name,tool_input,tool_use_id,permission_suggestionsNotification:message,title,notification_typePostToolUse:tool_name,tool_input,tool_response,tool_use_idPostToolUseFailure:tool_name,tool_input,tool_use_id,error,is_interruptStop:stop_hook_active,last_assistant_messageSubagentStart:agent_id,agent_typeTeammateIdle:teammate_name,team_nameTaskCompleted:task_id,task_subject,task_description,teammate_name,team_nameConfigChange:source,file_pathSubagentStop:stop_hook_active,agent_id,agent_type,agent_transcript_path,last_assistant_messagePreCompact:trigger,custom_instructionsWorktreeCreate:nameWorktreeRemove:worktree_path
Notes on tool events:
PostToolUsefires after a tool call that returnedsuccess=true.PostToolUseFailurefires after a tool call that returnedsuccess=false(or failed with an internal error).
Notes on when the multi-agent events fire:
SubagentStart: whenspawn_agent/spawn_teamcreates a new agent thread. The hook runs before the initial input is submitted, and anyadditionalContextoutput is injected into the spawned agent’s context.TeammateIdle: afterwait_teamreturns a final status for one or more teammates.TaskCompleted: whenteam_task_completeis called (and can block completion before it is persisted).WorktreeCreate: if configured, replaces the defaultgit worktree addbehavior. The hook must print the absolute path to the created worktree directory onstdout.WorktreeRemove: fired when an agent worktree is being cleaned up. For hook-created worktrees, Codex does not rungit worktree removeautomatically; pair this hook withworktree_createto handle cleanup.
If the hook exits 0, it may return a JSON object on stdout. (Exception: worktree_create uses stdout as a plain-text worktree path.) Codex recognizes these JSON keys:
- Context injection:
systemMessage/system_message(string)additionalContext/additional_context(string)hookSpecificOutput.additionalContext/hookSpecificOutput.additional_context(string)
- Input rewriting:
updatedInput/updated_input(any JSON value; only consumed bypre_tool_use)
- Blocking decisions (supported events only):
continue(boolean; Claude Code compatible): iffalse, stops processing and blocks execution. Takes precedence over any event-specific decision fields.decision(string)- Case-insensitive; accepted values:
- allow:
allow|approve|continue - deny:
deny|block|abort - ask:
ask
- allow:
- Prefer the canonical
allow|deny|askin new hooks. - For
user_prompt_submit,post_tool_use,post_tool_use_failure,stop,subagent_stop,config_change:denyblocks - For
pre_tool_use:deny|askblocks - For
permission_request:decisionis treated likepermissionDecisionbehavior
- Case-insensitive; accepted values:
reason/stopReason(string; used whendecisionblocks)
- Permission decisions (
permission_requestandpre_tool_use):permissionDecision/permission_decision(string; same accepted values asdecision; prefer canonicalallow|deny|ask)permissionDecisionReason/permission_decision_reason(string)hookSpecificOutput.decision.behavior(string; same accepted values asdecision) forpermission_request
Output precedence when multiple keys are present:
updatedInputprefers top-level overhookSpecificOutput.updatedInput.- Permission decisions prefer
hookSpecificOutput.permissionDecisionover top-levelpermissionDecision. continue=falsetakes precedence over any decision fields.- Block reason:
- If blocked by
continue=false:stopReason→reason→ fallback. - For
user_prompt_submit,stop,subagent_stop,config_change(decision-based blocking):reason→stopReason→ fallback. - For
pre_tool_use:- If blocked by
decision:reason→stopReason→hookSpecificOutput.permissionDecisionReason→ fallback. - If blocked by
permissionDecision:hookSpecificOutput.permissionDecisionReason→permissionDecisionReason→reason→stopReason→ fallback.
- If blocked by
- If blocked by
- Events that can be blocked (via
exit 2):user_prompt_submit,pre_tool_use,permission_requeststop,subagent_stopteammate_idle,task_completedconfig_change
- Events that honor
stdoutdecisions (decision/permissionDecision):user_prompt_submit,pre_tool_use,permission_requestpost_tool_use,post_tool_use_failurestop,subagent_stop,config_change
- Events that support
prompt/agenthooks:user_prompt_submit,pre_tool_use,permission_requestpost_tool_use,post_tool_use_failurestop,subagent_stoptask_completed
updatedInputis only consumed forpre_tool_use.worktree_createusesstdoutas a plain-text absolute path to the created worktree directory.- Permission decisions are consumed for
pre_tool_useandpermission_request:permission_request:allow|denybypasses the approval UI;askkeeps the UI path.pre_tool_use:deny|askblocks;allowcontinues.
[hooks]
[[hooks.pre_tool_use]]
name = "guard-shell"
command = ["python3", "/Users/me/.codex/hooks/pre_tool_use.py"]
timeout = 5
once = false
[hooks.pre_tool_use.matcher]
matcher = "shell"In a skill SKILL.md, add YAML frontmatter:
---
name: ralph-wiggum
description: Block stop until the user promises
hooks:
Stop:
- hooks:
- type: command
command: "python3 .claude/hooks/ralph-wiggum-stop-hook.py"
---Example: prompt-based guard on tool calls:
---
hooks:
PreToolUse:
- matcher: "shell|exec_command"
hooks:
- type: prompt
timeout: 15
prompt: |
Decide if this tool call is safe. Return JSON only: {"ok": true} or {"ok": false, "reason": "..."}.
$ARGUMENTS
---This is intended to run on your machine (not inside Codex's restricted test sandbox).
- Create a hook that logs every payload:
mkdir -p ~/.codex/hooks
cat > ~/.codex/hooks/log_event.py <<'PY'
#!/usr/bin/env python3
import json, os, sys, time
path = os.path.expanduser("~/.codex/hooks/e2e-events.jsonl")
payload = json.load(sys.stdin)
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps({"ts": time.time(), "hook_event_name": payload.get("hook_event_name"), "payload": payload}) + "\n")
print("{}")
PY
chmod +x ~/.codex/hooks/log_event.py- Wire it into
~/.codex/config.toml:
[hooks]
[[hooks.session_start]]
command = "python3 \"$HOME/.codex/hooks/log_event.py\""
[[hooks.pre_tool_use]]
command = "python3 \"$HOME/.codex/hooks/log_event.py\""
[[hooks.stop]]
command = "python3 \"$HOME/.codex/hooks/log_event.py\""- Trigger events and inspect the log:
: > ~/.codex/hooks/e2e-events.jsonl
codex exec "只回复 E2E_OK"
codex exec "请使用 shell 工具执行:echo hi"
jq -r '.hook_event_name' ~/.codex/hooks/e2e-events.jsonl | sort | uniq -c