hooks.md +645 −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 enabled by default. If you need to turn them off in `config.toml`,
13set:
14
15```toml
16[features]
17hooks = false
18```
19
20Use `hooks` as the canonical feature key. `codex_hooks` still works as a
21deprecated alias.
22
23Admins can force hooks off the same way in `requirements.toml` with
24`[features].hooks = false`.
25
26Runtime behavior to keep in mind:
27
28- Matching hooks from multiple files all run.
29- Multiple matching command hooks for the same event are launched concurrently,
30 so one hook cannot prevent another matching hook from starting.
31- Non-managed command hooks must be reviewed and trusted before they run.
32- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and
33 `Stop` run at turn scope.
34
35## Where Codex looks for hooks
36
37Codex discovers hooks next to active config layers in either of these forms:
38
39- `hooks.json`
40- inline `[hooks]` tables inside `config.toml`
41
42Installed plugins can also bundle lifecycle config through their plugin
43manifest or a default `hooks/hooks.json` file. See [Build
44plugins](https://developers.openai.com/codex/plugins/build#bundled-mcp-servers-and-lifecycle-config) for the
45plugin packaging rules.
46
47In practice, the four most useful locations are:
48
49- `~/.codex/hooks.json`
50- `~/.codex/config.toml`
51- `<repo>/.codex/hooks.json`
52- `<repo>/.codex/config.toml`
53
54If more than one hook source exists, Codex loads all matching hooks.
55Higher-precedence config layers don't replace lower-precedence hooks.
56If a single layer contains both `hooks.json` and inline `[hooks]`, Codex
57merges them and warns at startup. Prefer one representation per layer.
58
59Plugin hooks are off by default in this release. If
60`[features].plugin_hooks = true`, Codex can also discover hooks bundled with
61enabled plugins. Otherwise, enabled plugins won't run bundled hooks.
62
63Project-local hooks load only when the project `.codex/` layer is trusted. In
64untrusted projects, Codex still loads user and system hooks from their own
65active config layers.
66
67## Review and manage hooks
68
69Codex lists configured hooks before deciding which ones can run. Use `/hooks`
70in the CLI to inspect hook sources, review new or changed hooks, trust hooks, or
71disable individual non-managed hooks. If hooks need review at startup, Codex
72prints a warning that tells you to open `/hooks`.
73
74Managed hooks from system, MDM, cloud, or `requirements.toml` sources are marked
75as managed, trusted by policy, and can't be disabled from the user hook browser.
76
77## Config shape
78
79Hooks are organized in three levels:
80
81- A hook event such as `PreToolUse`, `PostToolUse`, or `Stop`
82- A matcher group that decides when that event matches
83- One or more hook handlers that run when the matcher group matches
84
85```json
86{
87 "hooks": {
88 "SessionStart": [
89 {
90 "matcher": "startup|resume",
91 "hooks": [
92 {
93 "type": "command",
94 "command": "python3 ~/.codex/hooks/session_start.py",
95 "statusMessage": "Loading session notes"
96 }
97 ]
98 }
99 ],
100 "PreToolUse": [
101 {
102 "matcher": "Bash",
103 "hooks": [
104 {
105 "type": "command",
106 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"",
107 "statusMessage": "Checking Bash command"
108 }
109 ]
110 }
111 ],
112 "PermissionRequest": [
113 {
114 "matcher": "Bash",
115 "hooks": [
116 {
117 "type": "command",
118 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/permission_request.py\"",
119 "statusMessage": "Checking approval request"
120 }
121 ]
122 }
123 ],
124 "PostToolUse": [
125 {
126 "matcher": "Bash",
127 "hooks": [
128 {
129 "type": "command",
130 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py\"",
131 "statusMessage": "Reviewing Bash output"
132 }
133 ]
134 }
135 ],
136 "UserPromptSubmit": [
137 {
138 "hooks": [
139 {
140 "type": "command",
141 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/user_prompt_submit_data_flywheel.py\""
142 }
143 ]
144 }
145 ],
146 "Stop": [
147 {
148 "hooks": [
149 {
150 "type": "command",
151 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_continue.py\"",
152 "timeout": 30
153 }
154 ]
155 }
156 ]
157 }
158}
159```
160
161Notes:
162
163- `timeout` is in seconds.
164- If `timeout` is omitted, Codex uses `600` seconds.
165- `statusMessage` is optional.
166- `async` is parsed, but async command hooks aren't supported yet. Codex skips
167 handlers with `async: true`.
168- Only `type: "command"` handlers run today. `prompt` and `agent` handlers are
169 parsed but skipped.
170- Commands run with the session `cwd` as their working directory.
171- For repo-local hooks, prefer resolving from the git root instead of using a
172 relative path such as `.codex/hooks/...`. Codex may be started from a
173 subdirectory, and a git-root-based path keeps the hook location stable.
174
175Equivalent inline TOML in `config.toml`:
176
177```toml
178[[hooks.PreToolUse]]
179matcher = "^Bash$"
180
181[[hooks.PreToolUse.hooks]]
182type = "command"
183command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py"'
184timeout = 30
185statusMessage = "Checking Bash command"
186
187[[hooks.PostToolUse]]
188matcher = "^Bash$"
189
190[[hooks.PostToolUse.hooks]]
191type = "command"
192command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py"'
193timeout = 30
194statusMessage = "Reviewing Bash output"
195```
196
197## Managed hooks from `requirements.toml`
198
199Enterprise-managed requirements can also define hooks inline under `[hooks]`.
200This is useful when admins want to enforce the hook configuration while
201delivering the actual scripts through MDM or another device-management system.
202To enforce managed hooks even for users who disabled hooks locally, pin
203`[features].hooks = true` in `requirements.toml` alongside `[hooks]`.
204
205```toml
206[features]
207hooks = true
208
209[hooks]
210managed_dir = "/enterprise/hooks"
211windows_managed_dir = 'C:\enterprise\hooks'
212
213[[hooks.PreToolUse]]
214matcher = "^Bash$"
215
216[[hooks.PreToolUse.hooks]]
217type = "command"
218command = "python3 /enterprise/hooks/pre_tool_use_policy.py"
219timeout = 30
220statusMessage = "Checking managed Bash command"
221```
222
223Notes for managed hooks:
224
225- `managed_dir` is used on macOS and Linux.
226- `windows_managed_dir` is used on Windows.
227- Codex doesn't distribute the scripts in `managed_dir`; your enterprise
228 tooling must install and update them separately.
229- Managed hook commands should use absolute script paths under the configured
230 managed directory.
231
232## Plugin-bundled hooks
233
234Plugin-bundled hooks are opt-in for this release. When
235`[features].plugin_hooks = true` and a plugin is enabled, Codex can load
236lifecycle hooks from that plugin alongside user, project, and managed hooks.
237
238```toml
239[features]
240plugin_hooks = true
241```
242
243By default, Codex looks for `hooks/hooks.json` inside the plugin root. A plugin
244manifest can override that default with a `hooks` entry in
245`.codex-plugin/plugin.json`. The manifest entry can be a `./`-prefixed path, an
246array of `./`-prefixed paths, an inline hooks object, or an array of inline
247hooks objects.
248
249```json
250{
251 "name": "repo-policy",
252 "hooks": "./hooks/hooks.json"
253}
254```
255
256Manifest hook paths are resolved relative to the plugin root and must stay
257inside that root. If a manifest defines `hooks`, Codex uses those manifest
258entries instead of the default `hooks/hooks.json`.
259
260Plugin hook commands receive these environment variables:
261
262- `PLUGIN_ROOT` is a Codex-specific extension that points to the installed
263 plugin root.
264- `PLUGIN_DATA` is a Codex-specific extension that points to the plugin's
265 writable data directory.
266- Codex also sets `CLAUDE_PLUGIN_ROOT` and `CLAUDE_PLUGIN_DATA` for
267 compatibility with existing plugin hooks.
268
269Plugin hooks use the same event schema as other hooks. They are non-managed
270hooks, so they require trust review before they run.
271
272## Matcher patterns
273
274The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
275`""`, or omit `matcher` entirely to match every occurrence of a supported
276event.
277
278Only some current Codex events honor `matcher`:
279
280| Event | What `matcher` filters | Notes |
281| ------------------- | ---------------------- | ------------------------------------------------------------ |
282| `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
283| `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
284| `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
285| `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
286| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
287| `Stop` | not supported | Any configured `matcher` is ignored for this event |
288
289\*For `apply_patch`, `matcher` values can also use `Edit` or `Write`.
290
291Examples:
292
293- `Bash`
294- `^apply_patch$`
295- `Edit|Write`
296- `mcp__filesystem__read_file`
297- `mcp__filesystem__.*`
298- `startup|resume|clear`
299
300## Common input fields
301
302Every command hook receives one JSON object on `stdin`.
303
304These are the shared fields you will usually use:
305
306| Field | Type | Meaning |
307| ----------------- | ---------------- | ------------------------------------------- |
308| `session_id` | `string` | Current session or thread id. |
309| `transcript_path` | `string \| null` | Path to the session transcript file, if any |
310| `cwd` | `string` | Working directory for the session |
311| `hook_event_name` | `string` | Current hook event name |
312| `model` | `string` | Codex-specific extension. Active model slug |
313
314Turn-scoped hooks list `turn_id` as a Codex-specific extension in their
315event-specific tables.
316
317`SessionStart`, `PreToolUse`, `PermissionRequest`, `PostToolUse`,
318`UserPromptSubmit`, and `Stop` also include `permission_mode`, which describes
319the current permission mode as `default`, `acceptEdits`, `plan`, `dontAsk`, or
320`bypassPermissions`.
321
322`transcript_path` points to a conversation transcript for convenience, but the
323transcript format is not a stable interface for hooks and may change over time.
324
325If you need the full wire format, see [Schemas](#schemas).
326
327## Common output fields
328
329`SessionStart`, `UserPromptSubmit`, and `Stop` support these shared JSON
330fields:
331
332```json
333{
334 "continue": true,
335 "stopReason": "optional",
336 "systemMessage": "optional",
337 "suppressOutput": false
338}
339```
340
341| Field | Effect |
342| ---------------- | ----------------------------------------------- |
343| `continue` | If `false`, marks that hook run as stopped |
344| `stopReason` | Recorded as the reason for stopping |
345| `systemMessage` | Surfaced as a warning in the UI or event stream |
346| `suppressOutput` | Parsed today but not yet implemented |
347
348Exit `0` with no output is treated as success and Codex continues.
349
350`PreToolUse` and `PermissionRequest` support `systemMessage`, but `continue`,
351`stopReason`, and `suppressOutput` aren't currently supported for those events.
352
353`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.
354`suppressOutput` is parsed but not currently supported for that event.
355
356## Hooks
357
358### SessionStart
359
360`matcher` is applied to `source` for this event.
361
362Fields in addition to [Common input fields](#common-input-fields):
363
364| Field | Type | Meaning |
365| -------- | -------- | -------------------------------------------------------- |
366| `source` | `string` | How the session started: `startup`, `resume`, or `clear` |
367
368Plain text on `stdout` is added as extra developer context.
369
370JSON on `stdout` supports [Common output fields](#common-output-fields) and this
371hook-specific shape:
372
373```json
374{
375 "hookSpecificOutput": {
376 "hookEventName": "SessionStart",
377 "additionalContext": "Load the workspace conventions before editing."
378 }
379}
380```
381
382That `additionalContext` text is added as extra developer context.
383
384### PreToolUse
385
386`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
387and MCP tool calls. It's still a guardrail rather than a complete enforcement
388boundary because Codex can often perform equivalent work through another
389supported tool path.
390
391This doesn't intercept all shell calls yet, only the simple ones. The newer
392 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
393 shell, but interception is incomplete. Similarly, this doesn't intercept
394 `WebSearch` or other non-shell, non-MCP tool calls.
395
396`matcher` is applied to `tool_name` and matcher aliases. For file edits through
397`apply_patch`, `matcher` values can use `apply_patch`, `Edit`, or `Write`; hook input
398still reports `tool_name: "apply_patch"`.
399
400Fields in addition to [Common input fields](#common-input-fields):
401
402| Field | Type | Meaning |
403| ------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
404| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
405| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
406| `tool_use_id` | `string` | Tool-call id for this invocation |
407| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all arguments. |
408
409Plain text on `stdout` is ignored.
410
411JSON on `stdout` can use `systemMessage`. To deny a supported tool call, return
412this hook-specific shape:
413
414```json
415{
416 "hookSpecificOutput": {
417 "hookEventName": "PreToolUse",
418 "permissionDecision": "deny",
419 "permissionDecisionReason": "Destructive command blocked by hook."
420 }
421}
422```
423
424Codex also accepts this older block shape:
425
426```json
427{
428 "decision": "block",
429 "reason": "Destructive command blocked by hook."
430}
431```
432
433You can also use exit code `2` and write the blocking reason to `stderr`.
434
435To add model-visible context without blocking, return
436`hookSpecificOutput.additionalContext`:
437
438```json
439{
440 "hookSpecificOutput": {
441 "hookEventName": "PreToolUse",
442 "additionalContext": "The pending command touches generated files."
443 }
444}
445```
446
447`permissionDecision: "ask"`, legacy `decision: "approve"`, `updatedInput`,
448`continue: false`, `stopReason`, and `suppressOutput` are parsed but not
449supported yet, so they fail open.
450
451### PermissionRequest
452
453`PermissionRequest` runs when Codex is about to ask for approval, such as a
454shell escalation or managed-network approval. It can allow the request, deny
455the request, or decline to decide and let the normal approval prompt continue.
456It doesn't run for commands that don't need approval.
457
458`matcher` is applied to `tool_name` and matcher aliases. Current canonical
459values include `Bash`, `apply_patch`, and MCP tool names such as
460`mcp__server__tool`; `apply_patch` also matches `Edit` and `Write`.
461
462Fields in addition to [Common input fields](#common-input-fields):
463
464| Field | Type | Meaning |
465| ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- |
466| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
467| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
468| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
469| `tool_input.description` | `string \| null` | Human-readable approval reason, when Codex has one |
470
471Plain text on `stdout` is ignored.
472
473Some tool inputs may include a human-readable description, but don't rely on a
474`tool_input.description` field for every tool.
475
476To approve the request, return:
477
478```json
479{
480 "hookSpecificOutput": {
481 "hookEventName": "PermissionRequest",
482 "decision": {
483 "behavior": "allow"
484 }
485 }
486}
487```
488
489To deny the request, return:
490
491```json
492{
493 "hookSpecificOutput": {
494 "hookEventName": "PermissionRequest",
495 "decision": {
496 "behavior": "deny",
497 "message": "Blocked by repository policy."
498 }
499 }
500}
501```
502
503If multiple matching hooks return decisions, any `deny` wins. Otherwise, an
504`allow` lets the request proceed without surfacing the approval prompt. If no
505matching hook decides, Codex uses the normal approval flow.
506
507Don't return `updatedInput`, `updatedPermissions`, or `interrupt` for
508`PermissionRequest`; those fields are reserved for future behavior and fail
509closed today.
510
511### PostToolUse
512
513`PostToolUse` runs after supported tools produce output, including Bash,
514`apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
515exit with a non-zero status. It can't undo side effects from the tool that
516already ran.
517
518This doesn't intercept all shell calls yet, only the simple ones. The newer
519 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
520 shell, but interception is incomplete. Similarly, this doesn't intercept
521 `WebSearch` or other non-shell, non-MCP tool calls.
522
523`matcher` is applied to `tool_name` and matcher aliases. For file edits through
524`apply_patch`, `matcher` values can use `apply_patch`, `Edit`, or `Write`; hook input
525still reports `tool_name: "apply_patch"`.
526
527Fields in addition to [Common input fields](#common-input-fields):
528
529| Field | Type | Meaning |
530| --------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
531| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
532| `tool_name` | `string` | Canonical hook tool name, such as `Bash`, `apply_patch`, or an MCP name like `mcp__fs__read` |
533| `tool_use_id` | `string` | Tool-call id for this invocation |
534| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all arguments. |
535| `tool_response` | `JSON value` | Tool-specific output. For MCP tools, this is the MCP call result. |
536
537Plain text on `stdout` is ignored.
538
539JSON on `stdout` can use `systemMessage` and this hook-specific shape:
540
541```json
542{
543 "decision": "block",
544 "reason": "The Bash output needs review before continuing.",
545 "hookSpecificOutput": {
546 "hookEventName": "PostToolUse",
547 "additionalContext": "The command updated generated files."
548 }
549}
550```
551
552That `additionalContext` text is added as extra developer context.
553
554For this event, `decision: "block"` doesn't undo the completed Bash command.
555Instead, Codex records the feedback, replaces the tool result with that
556feedback, and continues the model from the hook-provided message.
557
558You can also use exit code `2` and write the feedback reason to `stderr`.
559
560To stop normal processing of the original tool result after the command has
561already run, return `continue: false`. Codex will replace the tool result with
562your feedback or stop text and continue from there.
563
564`updatedMCPToolOutput` and `suppressOutput` are parsed but not supported yet,
565so they fail open.
566
567### UserPromptSubmit
568
569`matcher` isn't currently used for this event.
570
571Fields in addition to [Common input fields](#common-input-fields):
572
573| Field | Type | Meaning |
574| --------- | -------- | ---------------------------------------------- |
575| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
576| `prompt` | `string` | User prompt that's about to be sent |
577
578Plain text on `stdout` is added as extra developer context.
579
580JSON on `stdout` supports [Common output fields](#common-output-fields) and
581this hook-specific shape:
582
583```json
584{
585 "hookSpecificOutput": {
586 "hookEventName": "UserPromptSubmit",
587 "additionalContext": "Ask for a clearer reproduction before editing files."
588 }
589}
590```
591
592That `additionalContext` text is added as extra developer context.
593
594To block the prompt, return:
595
596```json
597{
598 "decision": "block",
599 "reason": "Ask for confirmation before doing that."
600}
601```
602
603You can also use exit code `2` and write the blocking reason to `stderr`.
604
605### Stop
606
607`matcher` isn't currently used for this event.
608
609Fields in addition to [Common input fields](#common-input-fields):
610
611| Field | Type | Meaning |
612| ------------------------ | ---------------- | ------------------------------------------------- |
613| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
614| `stop_hook_active` | `boolean` | Whether this turn was already continued by `Stop` |
615| `last_assistant_message` | `string \| null` | Latest assistant message text, if available |
616
617`Stop` expects JSON on `stdout` when it exits `0`. Plain text output is invalid
618for this event.
619
620JSON on `stdout` supports [Common output fields](#common-output-fields). To keep
621Codex going, return:
622
623```json
624{
625 "decision": "block",
626 "reason": "Run one more pass over the failing tests."
627}
628```
629
630You can also use exit code `2` and write the continuation reason to `stderr`.
631
632For this event, `decision: "block"` doesn't reject the turn. Instead, it tells
633Codex to continue and automatically creates a new continuation prompt that acts
634as a new user prompt, using your `reason` as that prompt text.
635
636If any matching `Stop` hook returns `continue: false`, that takes precedence
637over continuation decisions from other matching `Stop` hooks.
638
639## Schemas
640
641The linked `main` branch schemas may include hook fields that are not in the
642 current release. Use this page as the release behavior reference.
643
644If you need the exact current wire format, see the generated schemas in the
645[Codex GitHub repository](https://github.com/openai/codex/tree/main/codex-rs/hooks/schema/generated).