hooks.md +228 −69
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
12- Run a custom validation check when a conversation turn stops, enforcing standards9- 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
1512Hooks are behind a feature flag in `config.toml`:Hooks are enabled by default. If you need to turn them off in `config.toml`,
13set:
16 14
17```toml15```toml
18[features]16[features]
1917codex_hooks = truehooks = false
20```18```
21 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
22Runtime behavior to keep in mind:26Runtime behavior to keep in mind:
23 27
24- Matching hooks from multiple files all run.28- Matching hooks from multiple files all run.
25- Multiple matching command hooks for the same event are launched concurrently,29- Multiple matching command hooks for the same event are launched concurrently,
2630 so one hook can’t prevent another matching hook from starting. so one hook cannot prevent another matching hook from starting.
31- Non-managed command hooks must be reviewed and trusted before they run.
27- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and32- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and
28 `Stop` run at turn scope.33 `Stop` run at turn scope.
29- Hooks are currently disabled on Windows.
30 34
31## Where Codex looks for hooks35## Where Codex looks for hooks
32 36
3337Codex discovers `hooks.json` next to active config layers.Codex discovers hooks next to active config layers in either of these forms:
38
39- `hooks.json`
40- inline `[hooks]` tables inside `config.toml`
34 41
3542In practice, the two most useful locations are:Installed 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:
36 48
37- `~/.codex/hooks.json`49- `~/.codex/hooks.json`
50- `~/.codex/config.toml`
38- `<repo>/.codex/hooks.json`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.
39 58
4059If more than one `hooks.json` file exists, Codex loads all matching hooks.Plugin hooks are off by default in this release. If
4160Higher-precedence config layers don’t replace lower-precedence hooks.`[features].plugin_hooks = true`, Codex can also discover hooks bundled with
61enabled plugins. Otherwise, enabled plugins won't run bundled hooks.
42 62
43Project-local hooks load only when the project `.codex/` layer is trusted. In63Project-local hooks load only when the project `.codex/` layer is trusted. In
44untrusted projects, Codex still loads user and system hooks from their own64untrusted projects, Codex still loads user and system hooks from their own
45active config layers.65active config layers.
46 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
47## Config shape77## Config shape
48 78
49Hooks are organized in three levels:79Hooks are organized in three levels:
131Notes:161Notes:
132 162
133- `timeout` is in seconds.163- `timeout` is in seconds.
134- `timeoutSec` is also accepted as an alias.
135- If `timeout` is omitted, Codex uses `600` seconds.164- If `timeout` is omitted, Codex uses `600` seconds.
136- `statusMessage` is optional.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.
137- Commands run with the session `cwd` as their working directory.170- Commands run with the session `cwd` as their working directory.
138- For repo-local hooks, prefer resolving from the git root instead of using a171- For repo-local hooks, prefer resolving from the git root instead of using a
139 relative path such as `.codex/hooks/...`. Codex may be started from a172 relative path such as `.codex/hooks/...`. Codex may be started from a
140 subdirectory, and a git-root-based path keeps the hook location stable.173 subdirectory, and a git-root-based path keeps the hook location stable.
141 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
142## Matcher patterns272## Matcher patterns
143 273
144The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,274The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
148Only some current Codex events honor `matcher`:278Only some current Codex events honor `matcher`:
149 279
150| Event | What `matcher` filters | Notes |280| Event | What `matcher` filters | Notes |
151281| --- | --- | --- || ------------------- | ---------------------- | ------------------------------------------------------------ |
152282| `PermissionRequest` | tool name | Current Codex runtime only emits `Bash`. || `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
153283| `PostToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
154284| `PreToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
155285| `SessionStart` | start source | Current runtime values are `startup` and `resume`. || `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
156286| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event. || `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
157287| `Stop` | not supported | Any configured `matcher` is ignored for this event. || `Stop` | not supported | Any configured `matcher` is ignored for this event |
288
289\*For `apply_patch`, `matcher` values can also use `Edit` or `Write`.
158 290
159Examples:291Examples:
160 292
161- `Bash`293- `Bash`
162294- `startup|resume`- `^apply_patch$`
163- `Edit|Write`295- `Edit|Write`
164296 - `mcp__filesystem__read_file`
165297That last example is still a valid regex, but current Codex `PreToolUse` and- `mcp__filesystem__.*`
166298`PostToolUse` events only emit `Bash`, so it won’t match anything today.- `startup|resume|clear`
167 299
168## Common input fields300## Common input fields
169 301
172These are the shared fields you will usually use:304These are the shared fields you will usually use:
173 305
174| Field | Type | Meaning |306| Field | Type | Meaning |
175307| --- | --- | --- || ----------------- | ---------------- | ------------------------------------------- |
176| `session_id` | `string` | Current session or thread id. |308| `session_id` | `string` | Current session or thread id. |
177309| `transcript_path` | `string | null` | Path to the session transcript file, if any || `transcript_path` | `string \| null` | Path to the session transcript file, if any |
178| `cwd` | `string` | Working directory for the session |310| `cwd` | `string` | Working directory for the session |
179| `hook_event_name` | `string` | Current hook event name |311| `hook_event_name` | `string` | Current hook event name |
180312| `model` | `string` | Active model slug || `model` | `string` | Codex-specific extension. Active model slug |
181 313
182314Turn-scoped hooks list `turn_id` in their event-specific tables.Turn-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.
183 324
184If you need the full wire format, see [Schemas](#schemas).325If you need the full wire format, see [Schemas](#schemas).
185 326
221Fields in addition to [Common input fields](#common-input-fields):362Fields in addition to [Common input fields](#common-input-fields):
222 363
223| Field | Type | Meaning |364| Field | Type | Meaning |
224365| --- | --- | --- || -------- | -------- | -------------------------------------------------------- |
225366| `source` | `string` | How the session started: `startup` or `resume` || `source` | `string` | How the session started: `startup`, `resume`, or `clear` |
226 367
227Plain text on `stdout` is added as extra developer context.368Plain text on `stdout` is added as extra developer context.
228 369
242 383
243### PreToolUse384### PreToolUse
244 385
245386Work in progress`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
246387 and MCP tool calls. It's still a guardrail rather than a complete enforcement
247388Currently `PreToolUse` only supports Bash tool interception. The model canboundary because Codex can often perform equivalent work through another
248389still work around this by writing its own script to disk and then running thatsupported tool path.
249script with Bash, so treat this as a useful guardrail rather than a complete
250enforcement boundary
251 390
252This doesn't intercept all shell calls yet, only the simple ones. The newer391This doesn't intercept all shell calls yet, only the simple ones. The newer
253 `unified_exec` mechanism allows richer streaming stdin/stdout handling of392 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
254393shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
255394Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
256 395
257396`matcher` is applied to `tool_name`, which currently always equals `Bash`.`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"`.
258 399
259Fields in addition to [Common input fields](#common-input-fields):400Fields in addition to [Common input fields](#common-input-fields):
260 401
261| Field | Type | Meaning |402| Field | Type | Meaning |
262403| --- | --- | --- || ------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
263| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |404| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
264405| `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` |
265| `tool_use_id` | `string` | Tool-call id for this invocation |406| `tool_use_id` | `string` | Tool-call id for this invocation |
266407| `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 arguments. |
267 408
268Plain text on `stdout` is ignored.409Plain text on `stdout` is ignored.
269 410
270411JSON on `stdout` can use `systemMessage` and can block a Bash command with thisJSON on `stdout` can use `systemMessage`. To deny a supported tool call, return
271412hook-specific shape:this hook-specific shape:
272 413
273```json414```json
274{415{
291 432
292You can also use exit code `2` and write the blocking reason to `stderr`.433You can also use exit code `2` and write the blocking reason to `stderr`.
293 434
294435`permissionDecision: "allow"` and `"ask"`, legacy `decision: "approve"`,To add model-visible context without blocking, return
295436`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and`hookSpecificOutput.additionalContext`:
296`suppressOutput` are parsed but not supported yet, so they fail open.
297 437
298438### PermissionRequest```json
439{
440 "hookSpecificOutput": {
441 "hookEventName": "PreToolUse",
442 "additionalContext": "The pending command touches generated files."
443 }
444}
445```
299 446
300447Work in progress`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
301 452
302`PermissionRequest` runs when Codex is about to ask for approval, such as a453`PermissionRequest` runs when Codex is about to ask for approval, such as a
303shell escalation or managed-network approval. It can allow the request, deny454shell escalation or managed-network approval. It can allow the request, deny
304the request, or decline to decide and let the normal approval prompt continue.455the request, or decline to decide and let the normal approval prompt continue.
305It doesn't run for commands that don't need approval.456It doesn't run for commands that don't need approval.
306 457
307458`matcher` is applied to `tool_name`, which currently always equals `Bash`.`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`.
308 461
309Fields in addition to [Common input fields](#common-input-fields):462Fields in addition to [Common input fields](#common-input-fields):
310 463
311| Field | Type | Meaning |464| Field | Type | Meaning |
312465| --- | --- | --- || ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- |
313| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |466| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
314467| `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` |
315468| `tool_input.command` | `string` | Shell command associated with the approval request || `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
316469| `tool_input.description` | `string | null` | Human-readable approval reason, when Codex has one || `tool_input.description` | `string \| null` | Human-readable approval reason, when Codex has one |
317 470
318Plain text on `stdout` is ignored.471Plain text on `stdout` is ignored.
319 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
320To approve the request, return:476To approve the request, return:
321 477
322```json478```json
354 510
355### PostToolUse511### PostToolUse
356 512
357513Work in progress`PostToolUse` runs after supported tools produce output, including Bash,
358514 `apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
359515Currently `PostToolUse` only supports Bash tool results. It’s not limited toexit with a non-zero status. It can't undo side effects from the tool that
360516commands that exit successfully: non-interactive `exec_command` calls can stillalready ran.
361trigger `PostToolUse` when Codex emits a Bash post-tool payload. It can’t undo
362side effects from the command that already ran.
363 517
364This doesn't intercept all shell calls yet, only the simple ones. The newer518This doesn't intercept all shell calls yet, only the simple ones. The newer
365 `unified_exec` mechanism allows richer streaming stdin/stdout handling of519 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
366520shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
367521Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
368 522
369523`matcher` is applied to `tool_name`, which currently always equals `Bash`.`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"`.
370 526
371Fields in addition to [Common input fields](#common-input-fields):527Fields in addition to [Common input fields](#common-input-fields):
372 528
373| Field | Type | Meaning |529| Field | Type | Meaning |
374530| --- | --- | --- || --------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
375| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |531| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
376532| `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` |
377| `tool_use_id` | `string` | Tool-call id for this invocation |533| `tool_use_id` | `string` | Tool-call id for this invocation |
378534| `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 arguments. |
379535| `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. |
380 536
381Plain text on `stdout` is ignored.537Plain text on `stdout` is ignored.
382 538
415Fields in addition to [Common input fields](#common-input-fields):571Fields in addition to [Common input fields](#common-input-fields):
416 572
417| Field | Type | Meaning |573| Field | Type | Meaning |
418574| --- | --- | --- || --------- | -------- | ---------------------------------------------- |
419| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |575| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
420| `prompt` | `string` | User prompt that's about to be sent |576| `prompt` | `string` | User prompt that's about to be sent |
421 577
453Fields in addition to [Common input fields](#common-input-fields):609Fields in addition to [Common input fields](#common-input-fields):
454 610
455| Field | Type | Meaning |611| Field | Type | Meaning |
456612| --- | --- | --- || ------------------------ | ---------------- | ------------------------------------------------- |
457| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |613| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
458| `stop_hook_active` | `boolean` | Whether this turn was already continued by `Stop` |614| `stop_hook_active` | `boolean` | Whether this turn was already continued by `Stop` |
459615| `last_assistant_message` | `string | null` | Latest assistant message text, if available || `last_assistant_message` | `string \| null` | Latest assistant message text, if available |
460 616
461`Stop` expects JSON on `stdout` when it exits `0`. Plain text output is invalid617`Stop` expects JSON on `stdout` when it exits `0`. Plain text output is invalid
462for this event.618for this event.
482 638
483## Schemas639## Schemas
484 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
485If you need the exact current wire format, see the generated schemas in the644If you need the exact current wire format, see the generated schemas in the
486[Codex GitHub repository](https://github.com/openai/codex/tree/main/codex-rs/hooks/schema/generated).645[Codex GitHub repository](https://github.com/openai/codex/tree/main/codex-rs/hooks/schema/generated).