hooks.md +186 −50
1# Hooks1# Hooks
2 2
3Experimental. Hooks are under active development. Windows support temporarily
4disabled.
5
6Hooks are an extensibility framework for Codex. They allow3Hooks are an extensibility framework for Codex. They allow
7you to inject your own scripts into the agentic loop, enabling features such as:4you to inject your own scripts into the agentic loop, enabling features such as:
8 5
9- Send the conversation to a custom logging/analytics engine6- Send the conversation to a custom logging/analytics engine
10- Scan your team's prompts to block accidentally pasting API keys7- Scan your team's prompts to block accidentally pasting API keys
11- Summarize conversations to create persistent memories automatically8- Summarize conversations to create persistent memories automatically
129- Run a custom validator when a conversation turn stops, enforcing standards- Run a custom validation check when a conversation turn stops, enforcing standards
13- Customize prompting when in a certain directory10- Customize prompting when in a certain directory
14 11
15Hooks are behind a feature flag in `config.toml`:12Hooks are behind a feature flag in `config.toml`:
24- Matching hooks from multiple files all run.21- Matching hooks from multiple files all run.
25- Multiple matching command hooks for the same event are launched concurrently,22- Multiple matching command hooks for the same event are launched concurrently,
26 so one hook cannot prevent another matching hook from starting.23 so one hook cannot prevent another matching hook from starting.
2724- `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, and `Stop` run at turn- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and
2825 scope. `Stop` run at turn scope.
29- Hooks are currently disabled on Windows.
30 26
31## Where Codex looks for hooks27## Where Codex looks for hooks
32 28
3329Codex discovers `hooks.json` next to active config layers.Codex discovers hooks next to active config layers in either of these forms:
30
31- `hooks.json`
32- inline `[hooks]` tables inside `config.toml`
34 33
3534In practice, the two most useful locations are:In practice, the four most useful locations are:
36 35
37- `~/.codex/hooks.json`36- `~/.codex/hooks.json`
37- `~/.codex/config.toml`
38- `<repo>/.codex/hooks.json`38- `<repo>/.codex/hooks.json`
39- `<repo>/.codex/config.toml`
39 40
4041If more than one `hooks.json` file exists, Codex loads all matching hooks.If more than one hook source exists, Codex loads all matching hooks.
41Higher-precedence config layers do not replace lower-precedence hooks.42Higher-precedence config layers do not replace lower-precedence hooks.
43If a single layer contains both `hooks.json` and inline `[hooks]`, Codex
44merges them and warns at startup. Prefer one representation per layer.
45
46Project-local hooks load only when the project `.codex/` layer is trusted. In
47untrusted projects, Codex still loads user and system hooks from their own
48active config layers.
42 49
43## Config shape50## Config shape
44 51
75 ]82 ]
76 }83 }
77 ],84 ],
85 "PermissionRequest": [
86 {
87 "matcher": "Bash",
88 "hooks": [
89 {
90 "type": "command",
91 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/permission_request.py\"",
92 "statusMessage": "Checking approval request"
93 }
94 ]
95 }
96 ],
78 "PostToolUse": [97 "PostToolUse": [
79 {98 {
80 "matcher": "Bash",99 "matcher": "Bash",
115Notes:134Notes:
116 135
117- `timeout` is in seconds.136- `timeout` is in seconds.
118- `timeoutSec` is also accepted as an alias.
119- If `timeout` is omitted, Codex uses `600` seconds.137- If `timeout` is omitted, Codex uses `600` seconds.
120- `statusMessage` is optional.138- `statusMessage` is optional.
121- Commands run with the session `cwd` as their working directory.139- Commands run with the session `cwd` as their working directory.
123 relative path such as `.codex/hooks/...`. Codex may be started from a141 relative path such as `.codex/hooks/...`. Codex may be started from a
124 subdirectory, and a git-root-based path keeps the hook location stable.142 subdirectory, and a git-root-based path keeps the hook location stable.
125 143
144Equivalent inline TOML in `config.toml`:
145
146```toml
147[features]
148codex_hooks = true
149
150[[hooks.PreToolUse]]
151matcher = "^Bash$"
152
153[[hooks.PreToolUse.hooks]]
154type = "command"
155command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py"'
156timeout = 30
157statusMessage = "Checking Bash command"
158
159[[hooks.PostToolUse]]
160matcher = "^Bash$"
161
162[[hooks.PostToolUse.hooks]]
163type = "command"
164command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py"'
165timeout = 30
166statusMessage = "Reviewing Bash output"
167```
168
169## Managed hooks from `requirements.toml`
170
171Enterprise-managed requirements can also define hooks inline under `[hooks]`.
172This is useful when admins want to enforce the hook configuration while
173delivering the actual scripts through MDM or another device-management system.
174
175```toml
176[features]
177codex_hooks = true
178
179[hooks]
180managed_dir = "/enterprise/hooks"
181windows_managed_dir = 'C:\enterprise\hooks'
182
183[[hooks.PreToolUse]]
184matcher = "^Bash$"
185
186[[hooks.PreToolUse.hooks]]
187type = "command"
188command = "python3 /enterprise/hooks/pre_tool_use_policy.py"
189timeout = 30
190statusMessage = "Checking managed Bash command"
191```
192
193Notes for managed hooks:
194
195- `managed_dir` is used on macOS and Linux.
196- `windows_managed_dir` is used on Windows.
197- Codex does not distribute the scripts in `managed_dir`; your enterprise
198 tooling must install and update them separately.
199- Managed hook commands should use absolute script paths under the configured
200 managed directory.
201
126## Matcher patterns202## Matcher patterns
127 203
128The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,204The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
133 209
134| Event | What `matcher` filters | Notes |210| Event | What `matcher` filters | Notes |
135| --- | --- | --- |211| --- | --- | --- |
136212| `PostToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
137213| `PreToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
138214| `SessionStart` | start source | Current runtime values are `startup` and `resume`. || `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
139215| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event. || `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
140216| `Stop` | not supported | Any configured `matcher` is ignored for this event. || `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
217| `Stop` | not supported | Any configured `matcher` is ignored for this event |
218
219\*For `apply_patch`, matchers can also use `Edit` or `Write`.
141 220
142Examples:221Examples:
143 222
144- `Bash`223- `Bash`
145224- `startup|resume`- `^apply_patch$`
146- `Edit|Write`225- `Edit|Write`
147226 - `mcp__filesystem__read_file`
148227That last example is still a valid regex, but current Codex `PreToolUse` and- `mcp__filesystem__.*`
149228`PostToolUse` events only emit `Bash`, so it will not match anything today.- `startup|resume|clear`
150 229
151## Common input fields230## Common input fields
152 231
189 268
190Exit `0` with no output is treated as success and Codex continues.269Exit `0` with no output is treated as success and Codex continues.
191 270
192271`PreToolUse` supports `systemMessage`, but `continue`, `stopReason`, and`PreToolUse` and `PermissionRequest` support `systemMessage`, but `continue`,
193272`suppressOutput` are not currently supported for that event.`stopReason`, and `suppressOutput` aren't currently supported for those events.
194 273
195`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.274`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.
196`suppressOutput` is parsed but not currently supported for that event.275`suppressOutput` is parsed but not currently supported for that event.
225 304
226### PreToolUse305### PreToolUse
227 306
228307Work in progress`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
229308 and MCP tool calls. It is still a guardrail rather than a complete enforcement
230309Currently `PreToolUse` only supports Bash tool interception. The model canboundary because Codex can often perform equivalent work through another
231310still work around this by writing its own script to disk and then running thatsupported tool path.
232script with Bash, so treat this as a useful guardrail rather than a complete
233enforcement boundary
234 311
235This doesn't intercept all shell calls yet, only the simple ones. The newer312This doesn't intercept all shell calls yet, only the simple ones. The newer
236 `unified_exec` mechanism allows richer streaming stdin/stdout handling of313 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
237314shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
238315Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
239 316
240317`matcher` is applied to `tool_name`, which currently always equals `Bash`.`matcher` is applied to `tool_name` and matcher aliases. For file edits through
318`apply_patch`, matchers can use `apply_patch`, `Edit`, or `Write`; hook input
319still reports `tool_name: "apply_patch"`.
241 320
242Fields in addition to [Common input fields](#common-input-fields):321Fields in addition to [Common input fields](#common-input-fields):
243 322
244| Field | Type | Meaning |323| Field | Type | Meaning |
245| --- | --- | --- |324| --- | --- | --- |
246| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |325| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
247326| `tool_name` | `string` | Currently always `Bash` || `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
248| `tool_use_id` | `string` | Tool-call id for this invocation |327| `tool_use_id` | `string` | Tool-call id for this invocation |
249328| `tool_input.command` | `string` | Shell command Codex is about to run || `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
250 329
251Plain text on `stdout` is ignored.330Plain text on `stdout` is ignored.
252 331
278`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and357`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and
279`suppressOutput` are parsed but not supported yet, so they fail open.358`suppressOutput` are parsed but not supported yet, so they fail open.
280 359
281360### PostToolUse### PermissionRequest
361
362`PermissionRequest` runs when Codex is about to ask for approval, such as a
363shell escalation or managed-network approval. It can allow the request, deny
364the request, or decline to decide and let the normal approval prompt continue.
365It doesn't run for commands that don't need approval.
366
367`matcher` is applied to `tool_name` and matcher aliases. Current canonical
368values include `Bash`, `apply_patch`, and MCP tool names such as
369`mcp__server__tool`; `apply_patch` also matches `Edit` and `Write`.
370
371Fields in addition to [Common input fields](#common-input-fields):
372
373| Field | Type | Meaning |
374| --- | --- | --- |
375| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
376| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
377| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
378| `tool_input.description` | `string | null` | Human-readable approval reason, when Codex has one |
379
380Plain text on `stdout` is ignored.
381
382To approve the request, return:
383
384```json
385{
386 "hookSpecificOutput": {
387 "hookEventName": "PermissionRequest",
388 "decision": {
389 "behavior": "allow"
390 }
391 }
392}
393```
282 394
283395Work in progressTo deny the request, return:
396
397```json
398{
399 "hookSpecificOutput": {
400 "hookEventName": "PermissionRequest",
401 "decision": {
402 "behavior": "deny",
403 "message": "Blocked by repository policy."
404 }
405 }
406}
407```
408
409If multiple matching hooks return decisions, any `deny` wins. Otherwise, an
410`allow` lets the request proceed without surfacing the approval prompt. If no
411matching hook decides, Codex uses the normal approval flow.
412
413Don't return `updatedInput`, `updatedPermissions`, or `interrupt` for
414`PermissionRequest`; those fields are reserved for future behavior and fail
415closed today.
416
417### PostToolUse
284 418
285419Currently `PostToolUse` only supports Bash tool results. It is not limited to`PostToolUse` runs after supported tools produce output, including Bash,
286420commands that exit successfully: non-interactive `exec_command` calls can still`apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
287421trigger `PostToolUse` when Codex emits a Bash post-tool payload. It cannot undoexit with a non-zero status. It can't undo side effects from the tool that
288422side effects from the command that already ran.already ran.
289 423
290This doesn't intercept all shell calls yet, only the simple ones. The newer424This doesn't intercept all shell calls yet, only the simple ones. The newer
291 `unified_exec` mechanism allows richer streaming stdin/stdout handling of425 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
292426shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
293427Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
294 428
295429`matcher` is applied to `tool_name`, which currently always equals `Bash`.`matcher` is applied to `tool_name` and matcher aliases. For file edits through
430`apply_patch`, matchers can use `apply_patch`, `Edit`, or `Write`; hook input
431still reports `tool_name: "apply_patch"`.
296 432
297Fields in addition to [Common input fields](#common-input-fields):433Fields in addition to [Common input fields](#common-input-fields):
298 434
299| Field | Type | Meaning |435| Field | Type | Meaning |
300| --- | --- | --- |436| --- | --- | --- |
301| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |437| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
302438| `tool_name` | `string` | Currently always `Bash` || `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
303| `tool_use_id` | `string` | Tool-call id for this invocation |439| `tool_use_id` | `string` | Tool-call id for this invocation |
304440| `tool_input.command` | `string` | Shell command Codex just ran || `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
305441| `tool_response` | `JSON value` | Bash tool output payload. Today this is usually a JSON string || `tool_response` | `JSON value` | Tool-specific output. For MCP tools, this is the MCP call result. |
306 442
307Plain text on `stdout` is ignored.443Plain text on `stdout` is ignored.
308 444
321 457
322That `additionalContext` text is added as extra developer context.458That `additionalContext` text is added as extra developer context.
323 459
324460For this event, `decision: "block"` does not undo the completed Bash command.For this event, `decision: "block"` doesn't undo the completed Bash command.
325Instead, Codex records the feedback, replaces the tool result with that461Instead, Codex records the feedback, replaces the tool result with that
326feedback, and continues the model from the hook-provided message.462feedback, and continues the model from the hook-provided message.
327 463
336 472
337### UserPromptSubmit473### UserPromptSubmit
338 474
339475`matcher` is not currently used for this event.`matcher` isn't currently used for this event.
340 476
341Fields in addition to [Common input fields](#common-input-fields):477Fields in addition to [Common input fields](#common-input-fields):
342 478
343| Field | Type | Meaning |479| Field | Type | Meaning |
344| --- | --- | --- |480| --- | --- | --- |
345| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |481| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
346482| `prompt` | `string` | User prompt that is about to be sent || `prompt` | `string` | User prompt that's about to be sent |
347 483
348Plain text on `stdout` is added as extra developer context.484Plain text on `stdout` is added as extra developer context.
349 485
374 510
375### Stop511### Stop
376 512
377513`matcher` is not currently used for this event.`matcher` isn't currently used for this event.
378 514
379Fields in addition to [Common input fields](#common-input-fields):515Fields in addition to [Common input fields](#common-input-fields):
380 516
399 535
400You can also use exit code `2` and write the continuation reason to `stderr`.536You can also use exit code `2` and write the continuation reason to `stderr`.
401 537
402538For this event, `decision: "block"` does not reject the turn. Instead, it tellsFor this event, `decision: "block"` doesn't reject the turn. Instead, it tells
403Codex to continue and automatically creates a new continuation prompt that acts539Codex to continue and automatically creates a new continuation prompt that acts
404as a new user prompt, using your `reason` as that prompt text.540as a new user prompt, using your `reason` as that prompt text.
405 541