hooks.md +548 −0 added
1# Hooks
2
3Hooks are an extensibility framework for Codex. They allow
4you to inject your own scripts into the agentic loop, enabling features such as:
5
6- Send the conversation to a custom logging/analytics engine
7- Scan your team's prompts to block accidentally pasting API keys
8- Summarize conversations to create persistent memories automatically
9- Run a custom validation check when a conversation turn stops, enforcing standards
10- Customize prompting when in a certain directory
11
12Hooks are behind a feature flag in `config.toml`:
13
14```toml
15[features]
16codex_hooks = true
17```
18
19Runtime behavior to keep in mind:
20
21- Matching hooks from multiple files all run.
22- Multiple matching command hooks for the same event are launched concurrently,
23 so one hook cannot prevent another matching hook from starting.
24- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and
25 `Stop` run at turn scope.
26
27## Where Codex looks for hooks
28
29Codex discovers hooks next to active config layers in either of these forms:
30
31- `hooks.json`
32- inline `[hooks]` tables inside `config.toml`
33
34In practice, the four most useful locations are:
35
36- `~/.codex/hooks.json`
37- `~/.codex/config.toml`
38- `<repo>/.codex/hooks.json`
39- `<repo>/.codex/config.toml`
40
41If more than one hook source exists, Codex loads all matching 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.
49
50## Config shape
51
52Hooks are organized in three levels:
53
54- A hook event such as `PreToolUse`, `PostToolUse`, or `Stop`
55- A matcher group that decides when that event matches
56- One or more hook handlers that run when the matcher group matches
57
58```json
59{
60 "hooks": {
61 "SessionStart": [
62 {
63 "matcher": "startup|resume",
64 "hooks": [
65 {
66 "type": "command",
67 "command": "python3 ~/.codex/hooks/session_start.py",
68 "statusMessage": "Loading session notes"
69 }
70 ]
71 }
72 ],
73 "PreToolUse": [
74 {
75 "matcher": "Bash",
76 "hooks": [
77 {
78 "type": "command",
79 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"",
80 "statusMessage": "Checking Bash command"
81 }
82 ]
83 }
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 ],
97 "PostToolUse": [
98 {
99 "matcher": "Bash",
100 "hooks": [
101 {
102 "type": "command",
103 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py\"",
104 "statusMessage": "Reviewing Bash output"
105 }
106 ]
107 }
108 ],
109 "UserPromptSubmit": [
110 {
111 "hooks": [
112 {
113 "type": "command",
114 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/user_prompt_submit_data_flywheel.py\""
115 }
116 ]
117 }
118 ],
119 "Stop": [
120 {
121 "hooks": [
122 {
123 "type": "command",
124 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_continue.py\"",
125 "timeout": 30
126 }
127 ]
128 }
129 ]
130 }
131}
132```
133
134Notes:
135
136- `timeout` is in seconds.
137- If `timeout` is omitted, Codex uses `600` seconds.
138- `statusMessage` is optional.
139- Commands run with the session `cwd` as their working directory.
140- For repo-local hooks, prefer resolving from the git root instead of using a
141 relative path such as `.codex/hooks/...`. Codex may be started from a
142 subdirectory, and a git-root-based path keeps the hook location stable.
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
202## Matcher patterns
203
204The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
205`""`, or omit `matcher` entirely to match every occurrence of a supported
206event.
207
208Only some current Codex events honor `matcher`:
209
210| Event | What `matcher` filters | Notes |
211| --- | --- | --- |
212| `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
213| `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
214| `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
215| `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
216| `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`.
220
221Examples:
222
223- `Bash`
224- `^apply_patch$`
225- `Edit|Write`
226- `mcp__filesystem__read_file`
227- `mcp__filesystem__.*`
228- `startup|resume|clear`
229
230## Common input fields
231
232Every command hook receives one JSON object on `stdin`.
233
234These are the shared fields you will usually use:
235
236| Field | Type | Meaning |
237| --- | --- | --- |
238| `session_id` | `string` | Current session or thread id. |
239| `transcript_path` | `string | null` | Path to the session transcript file, if any |
240| `cwd` | `string` | Working directory for the session |
241| `hook_event_name` | `string` | Current hook event name |
242| `model` | `string` | Active model slug |
243
244Turn-scoped hooks list `turn_id` in their event-specific tables.
245
246If you need the full wire format, see [Schemas](#schemas).
247
248## Common output fields
249
250`SessionStart`, `UserPromptSubmit`, and `Stop` support these shared JSON
251fields:
252
253```json
254{
255 "continue": true,
256 "stopReason": "optional",
257 "systemMessage": "optional",
258 "suppressOutput": false
259}
260```
261
262| Field | Effect |
263| ---------------- | ----------------------------------------------- |
264| `continue` | If `false`, marks that hook run as stopped |
265| `stopReason` | Recorded as the reason for stopping |
266| `systemMessage` | Surfaced as a warning in the UI or event stream |
267| `suppressOutput` | Parsed today but not yet implemented |
268
269Exit `0` with no output is treated as success and Codex continues.
270
271`PreToolUse` and `PermissionRequest` support `systemMessage`, but `continue`,
272`stopReason`, and `suppressOutput` aren't currently supported for those events.
273
274`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.
275`suppressOutput` is parsed but not currently supported for that event.
276
277## Hooks
278
279### SessionStart
280
281`matcher` is applied to `source` for this event.
282
283Fields in addition to [Common input fields](#common-input-fields):
284
285| Field | Type | Meaning |
286| --- | --- | --- |
287| `source` | `string` | How the session started: `startup` or `resume` |
288
289Plain text on `stdout` is added as extra developer context.
290
291JSON on `stdout` supports [Common output fields](#common-output-fields) and this
292hook-specific shape:
293
294```json
295{
296 "hookSpecificOutput": {
297 "hookEventName": "SessionStart",
298 "additionalContext": "Load the workspace conventions before editing."
299 }
300}
301```
302
303That `additionalContext` text is added as extra developer context.
304
305### PreToolUse
306
307`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
308and MCP tool calls. It is still a guardrail rather than a complete enforcement
309boundary because Codex can often perform equivalent work through another
310supported tool path.
311
312This doesn't intercept all shell calls yet, only the simple ones. The newer
313 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
314 shell, but interception is incomplete. Similarly, this doesn't intercept
315 `WebSearch` or other non-shell, non-MCP tool calls.
316
317`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"`.
320
321Fields in addition to [Common input fields](#common-input-fields):
322
323| Field | Type | Meaning |
324| --- | --- | --- |
325| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
326| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
327| `tool_use_id` | `string` | Tool-call id for this invocation |
328| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
329
330Plain text on `stdout` is ignored.
331
332JSON on `stdout` can use `systemMessage` and can block a Bash command with this
333hook-specific shape:
334
335```json
336{
337 "hookSpecificOutput": {
338 "hookEventName": "PreToolUse",
339 "permissionDecision": "deny",
340 "permissionDecisionReason": "Destructive command blocked by hook."
341 }
342}
343```
344
345Codex also accepts this older block shape:
346
347```json
348{
349 "decision": "block",
350 "reason": "Destructive command blocked by hook."
351}
352```
353
354You can also use exit code `2` and write the blocking reason to `stderr`.
355
356`permissionDecision: "allow"` and `"ask"`, legacy `decision: "approve"`,
357`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and
358`suppressOutput` are parsed but not supported yet, so they fail open.
359
360### 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```
394
395To 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
418
419`PostToolUse` runs after supported tools produce output, including Bash,
420`apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
421exit with a non-zero status. It can't undo side effects from the tool that
422already ran.
423
424This doesn't intercept all shell calls yet, only the simple ones. The newer
425 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
426 shell, but interception is incomplete. Similarly, this doesn't intercept
427 `WebSearch` or other non-shell, non-MCP tool calls.
428
429`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"`.
432
433Fields in addition to [Common input fields](#common-input-fields):
434
435| Field | Type | Meaning |
436| --- | --- | --- |
437| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
438| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
439| `tool_use_id` | `string` | Tool-call id for this invocation |
440| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
441| `tool_response` | `JSON value` | Tool-specific output. For MCP tools, this is the MCP call result. |
442
443Plain text on `stdout` is ignored.
444
445JSON on `stdout` can use `systemMessage` and this hook-specific shape:
446
447```json
448{
449 "decision": "block",
450 "reason": "The Bash output needs review before continuing.",
451 "hookSpecificOutput": {
452 "hookEventName": "PostToolUse",
453 "additionalContext": "The command updated generated files."
454 }
455}
456```
457
458That `additionalContext` text is added as extra developer context.
459
460For this event, `decision: "block"` doesn't undo the completed Bash command.
461Instead, Codex records the feedback, replaces the tool result with that
462feedback, and continues the model from the hook-provided message.
463
464You can also use exit code `2` and write the feedback reason to `stderr`.
465
466To stop normal processing of the original tool result after the command has
467already run, return `continue: false`. Codex will replace the tool result with
468your feedback or stop text and continue from there.
469
470`updatedMCPToolOutput` and `suppressOutput` are parsed but not supported yet,
471so they fail open.
472
473### UserPromptSubmit
474
475`matcher` isn't currently used for this event.
476
477Fields in addition to [Common input fields](#common-input-fields):
478
479| Field | Type | Meaning |
480| --- | --- | --- |
481| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
482| `prompt` | `string` | User prompt that's about to be sent |
483
484Plain text on `stdout` is added as extra developer context.
485
486JSON on `stdout` supports [Common output fields](#common-output-fields) and
487this hook-specific shape:
488
489```json
490{
491 "hookSpecificOutput": {
492 "hookEventName": "UserPromptSubmit",
493 "additionalContext": "Ask for a clearer reproduction before editing files."
494 }
495}
496```
497
498That `additionalContext` text is added as extra developer context.
499
500To block the prompt, return:
501
502```json
503{
504 "decision": "block",
505 "reason": "Ask for confirmation before doing that."
506}
507```
508
509You can also use exit code `2` and write the blocking reason to `stderr`.
510
511### Stop
512
513`matcher` isn't currently used for this event.
514
515Fields in addition to [Common input fields](#common-input-fields):
516
517| Field | Type | Meaning |
518| --- | --- | --- |
519| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
520| `stop_hook_active` | `boolean` | Whether this turn was already continued by `Stop` |
521| `last_assistant_message` | `string | null` | Latest assistant message text, if available |
522
523`Stop` expects JSON on `stdout` when it exits `0`. Plain text output is invalid
524for this event.
525
526JSON on `stdout` supports [Common output fields](#common-output-fields). To keep
527Codex going, return:
528
529```json
530{
531 "decision": "block",
532 "reason": "Run one more pass over the failing tests."
533}
534```
535
536You can also use exit code `2` and write the continuation reason to `stderr`.
537
538For this event, `decision: "block"` doesn't reject the turn. Instead, it tells
539Codex to continue and automatically creates a new continuation prompt that acts
540as a new user prompt, using your `reason` as that prompt text.
541
542If any matching `Stop` hook returns `continue: false`, that takes precedence
543over continuation decisions from other matching `Stop` hooks.
544
545## Schemas
546
547If you need the exact current wire format, see the generated schemas in the
548[Codex GitHub repository](https://github.com/openai/codex/tree/main/codex-rs/hooks/schema/generated).