hooks.md +197 −42
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`
33
34Installed plugins can also bundle lifecycle config through their plugin
35manifest or a default `hooks/hooks.json` file. See [Build
36plugins](https://developers.openai.com/codex/plugins/build#bundled-mcp-servers-and-lifecycle-config) for the
37plugin packaging rules.
34 38
3539In practice, the two most useful locations are:In practice, the four most useful locations are:
36 40
37- `~/.codex/hooks.json`41- `~/.codex/hooks.json`
42- `~/.codex/config.toml`
38- `<repo>/.codex/hooks.json`43- `<repo>/.codex/hooks.json`
44- `<repo>/.codex/config.toml`
39 45
4046If 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.47Higher-precedence config layers do not replace lower-precedence hooks.
48If a single layer contains both `hooks.json` and inline `[hooks]`, Codex
49merges them and warns at startup. Prefer one representation per layer.
50
51Project-local hooks load only when the project `.codex/` layer is trusted. In
52untrusted projects, Codex still loads user and system hooks from their own
53active config layers.
42 54
43## Config shape55## Config shape
44 56
75 ]87 ]
76 }88 }
77 ],89 ],
90 "PermissionRequest": [
91 {
92 "matcher": "Bash",
93 "hooks": [
94 {
95 "type": "command",
96 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/permission_request.py\"",
97 "statusMessage": "Checking approval request"
98 }
99 ]
100 }
101 ],
78 "PostToolUse": [102 "PostToolUse": [
79 {103 {
80 "matcher": "Bash",104 "matcher": "Bash",
115Notes:139Notes:
116 140
117- `timeout` is in seconds.141- `timeout` is in seconds.
118- `timeoutSec` is also accepted as an alias.
119- If `timeout` is omitted, Codex uses `600` seconds.142- If `timeout` is omitted, Codex uses `600` seconds.
120- `statusMessage` is optional.143- `statusMessage` is optional.
121- Commands run with the session `cwd` as their working directory.144- Commands run with the session `cwd` as their working directory.
123 relative path such as `.codex/hooks/...`. Codex may be started from a146 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.147 subdirectory, and a git-root-based path keeps the hook location stable.
125 148
149Equivalent inline TOML in `config.toml`:
150
151```toml
152[features]
153codex_hooks = true
154
155[[hooks.PreToolUse]]
156matcher = "^Bash$"
157
158[[hooks.PreToolUse.hooks]]
159type = "command"
160command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py"'
161timeout = 30
162statusMessage = "Checking Bash command"
163
164[[hooks.PostToolUse]]
165matcher = "^Bash$"
166
167[[hooks.PostToolUse.hooks]]
168type = "command"
169command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py"'
170timeout = 30
171statusMessage = "Reviewing Bash output"
172```
173
174## Managed hooks from `requirements.toml`
175
176Enterprise-managed requirements can also define hooks inline under `[hooks]`.
177This is useful when admins want to enforce the hook configuration while
178delivering the actual scripts through MDM or another device-management system.
179
180```toml
181[features]
182codex_hooks = true
183
184[hooks]
185managed_dir = "/enterprise/hooks"
186windows_managed_dir = 'C:\enterprise\hooks'
187
188[[hooks.PreToolUse]]
189matcher = "^Bash$"
190
191[[hooks.PreToolUse.hooks]]
192type = "command"
193command = "python3 /enterprise/hooks/pre_tool_use_policy.py"
194timeout = 30
195statusMessage = "Checking managed Bash command"
196```
197
198Notes for managed hooks:
199
200- `managed_dir` is used on macOS and Linux.
201- `windows_managed_dir` is used on Windows.
202- Codex does not distribute the scripts in `managed_dir`; your enterprise
203 tooling must install and update them separately.
204- Managed hook commands should use absolute script paths under the configured
205 managed directory.
206
126## Matcher patterns207## Matcher patterns
127 208
128The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,209The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
133 214
134| Event | What `matcher` filters | Notes |215| Event | What `matcher` filters | Notes |
135| --- | --- | --- |216| --- | --- | --- |
136217| `PostToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
137218| `PreToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
138219| `SessionStart` | start source | Current runtime values are `startup` and `resume`. || `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
139220| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event. || `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
140221| `Stop` | not supported | Any configured `matcher` is ignored for this event. || `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
222| `Stop` | not supported | Any configured `matcher` is ignored for this event |
223
224\*For `apply_patch`, matchers can also use `Edit` or `Write`.
141 225
142Examples:226Examples:
143 227
144- `Bash`228- `Bash`
145229- `startup|resume`- `^apply_patch$`
146- `Edit|Write`230- `Edit|Write`
147231 - `mcp__filesystem__read_file`
148232That last example is still a valid regex, but current Codex `PreToolUse` and- `mcp__filesystem__.*`
149233`PostToolUse` events only emit `Bash`, so it will not match anything today.- `startup|resume|clear`
150 234
151## Common input fields235## Common input fields
152 236
189 273
190Exit `0` with no output is treated as success and Codex continues.274Exit `0` with no output is treated as success and Codex continues.
191 275
192276`PreToolUse` supports `systemMessage`, but `continue`, `stopReason`, and`PreToolUse` and `PermissionRequest` support `systemMessage`, but `continue`,
193277`suppressOutput` are not currently supported for that event.`stopReason`, and `suppressOutput` aren't currently supported for those events.
194 278
195`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.279`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.
196`suppressOutput` is parsed but not currently supported for that event.280`suppressOutput` is parsed but not currently supported for that event.
225 309
226### PreToolUse310### PreToolUse
227 311
228312Currently `PreToolUse` only supports Bash tool interception. The model can`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
229313still work around this by writing its own script to disk and then running thatand MCP tool calls. It is still a guardrail rather than a complete enforcement
230314script with Bash, so treat this as a useful guardrail rather than a completeboundary because Codex can often perform equivalent work through another
231315enforcement boundary.supported tool path.
316
317This doesn't intercept all shell calls yet, only the simple ones. The newer
318 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
319 shell, but interception is incomplete. Similarly, this doesn't intercept
320 `WebSearch` or other non-shell, non-MCP tool calls.
232 321
233322`matcher` is applied to `tool_name`, which currently always equals `Bash`.`matcher` is applied to `tool_name` and matcher aliases. For file edits through
323`apply_patch`, matchers can use `apply_patch`, `Edit`, or `Write`; hook input
324still reports `tool_name: "apply_patch"`.
234 325
235Fields in addition to [Common input fields](#common-input-fields):326Fields in addition to [Common input fields](#common-input-fields):
236 327
237| Field | Type | Meaning |328| Field | Type | Meaning |
238| --- | --- | --- |329| --- | --- | --- |
239| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |330| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
240331| `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` |
241| `tool_use_id` | `string` | Tool-call id for this invocation |332| `tool_use_id` | `string` | Tool-call id for this invocation |
242333| `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. |
243 334
244Plain text on `stdout` is ignored.335Plain text on `stdout` is ignored.
245 336
271`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and362`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and
272`suppressOutput` are parsed but not supported yet, so they fail open.363`suppressOutput` are parsed but not supported yet, so they fail open.
273 364
365### PermissionRequest
366
367`PermissionRequest` runs when Codex is about to ask for approval, such as a
368shell escalation or managed-network approval. It can allow the request, deny
369the request, or decline to decide and let the normal approval prompt continue.
370It doesn't run for commands that don't need approval.
371
372`matcher` is applied to `tool_name` and matcher aliases. Current canonical
373values include `Bash`, `apply_patch`, and MCP tool names such as
374`mcp__server__tool`; `apply_patch` also matches `Edit` and `Write`.
375
376Fields in addition to [Common input fields](#common-input-fields):
377
378| Field | Type | Meaning |
379| --- | --- | --- |
380| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
381| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
382| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
383| `tool_input.description` | `string | null` | Human-readable approval reason, when Codex has one |
384
385Plain text on `stdout` is ignored.
386
387To approve the request, return:
388
389```json
390{
391 "hookSpecificOutput": {
392 "hookEventName": "PermissionRequest",
393 "decision": {
394 "behavior": "allow"
395 }
396 }
397}
398```
399
400To deny the request, return:
401
402```json
403{
404 "hookSpecificOutput": {
405 "hookEventName": "PermissionRequest",
406 "decision": {
407 "behavior": "deny",
408 "message": "Blocked by repository policy."
409 }
410 }
411}
412```
413
414If multiple matching hooks return decisions, any `deny` wins. Otherwise, an
415`allow` lets the request proceed without surfacing the approval prompt. If no
416matching hook decides, Codex uses the normal approval flow.
417
418Don't return `updatedInput`, `updatedPermissions`, or `interrupt` for
419`PermissionRequest`; those fields are reserved for future behavior and fail
420closed today.
421
274### PostToolUse422### PostToolUse
275 423
276424Currently `PostToolUse` only supports Bash tool results. It is not limited to`PostToolUse` runs after supported tools produce output, including Bash,
277425commands that exit successfully: non-interactive `exec_command` calls can still`apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
278426trigger `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
279427side effects from the command that already ran.already ran.
428
429This doesn't intercept all shell calls yet, only the simple ones. The newer
430 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
431 shell, but interception is incomplete. Similarly, this doesn't intercept
432 `WebSearch` or other non-shell, non-MCP tool calls.
280 433
281434`matcher` is applied to `tool_name`, which currently always equals `Bash`.`matcher` is applied to `tool_name` and matcher aliases. For file edits through
435`apply_patch`, matchers can use `apply_patch`, `Edit`, or `Write`; hook input
436still reports `tool_name: "apply_patch"`.
282 437
283Fields in addition to [Common input fields](#common-input-fields):438Fields in addition to [Common input fields](#common-input-fields):
284 439
285| Field | Type | Meaning |440| Field | Type | Meaning |
286| --- | --- | --- |441| --- | --- | --- |
287| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |442| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
288443| `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` |
289| `tool_use_id` | `string` | Tool-call id for this invocation |444| `tool_use_id` | `string` | Tool-call id for this invocation |
290445| `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. |
291446| `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. |
292 447
293Plain text on `stdout` is ignored.448Plain text on `stdout` is ignored.
294 449
307 462
308That `additionalContext` text is added as extra developer context.463That `additionalContext` text is added as extra developer context.
309 464
310465For this event, `decision: "block"` does not undo the completed Bash command.For this event, `decision: "block"` doesn't undo the completed Bash command.
311Instead, Codex records the feedback, replaces the tool result with that466Instead, Codex records the feedback, replaces the tool result with that
312feedback, and continues the model from the hook-provided message.467feedback, and continues the model from the hook-provided message.
313 468
322 477
323### UserPromptSubmit478### UserPromptSubmit
324 479
325480`matcher` is not currently used for this event.`matcher` isn't currently used for this event.
326 481
327Fields in addition to [Common input fields](#common-input-fields):482Fields in addition to [Common input fields](#common-input-fields):
328 483
329| Field | Type | Meaning |484| Field | Type | Meaning |
330| --- | --- | --- |485| --- | --- | --- |
331| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |486| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
332487| `prompt` | `string` | User prompt that is about to be sent || `prompt` | `string` | User prompt that's about to be sent |
333 488
334Plain text on `stdout` is added as extra developer context.489Plain text on `stdout` is added as extra developer context.
335 490
360 515
361### Stop516### Stop
362 517
363518`matcher` is not currently used for this event.`matcher` isn't currently used for this event.
364 519
365Fields in addition to [Common input fields](#common-input-fields):520Fields in addition to [Common input fields](#common-input-fields):
366 521
385 540
386You can also use exit code `2` and write the continuation reason to `stderr`.541You can also use exit code `2` and write the continuation reason to `stderr`.
387 542
388543For this event, `decision: "block"` does not reject the turn. Instead, it tellsFor this event, `decision: "block"` doesn't reject the turn. Instead, it tells
389Codex to continue and automatically creates a new continuation prompt that acts544Codex to continue and automatically creates a new continuation prompt that acts
390as a new user prompt, using your `reason` as that prompt text.545as a new user prompt, using your `reason` as that prompt text.
391 546