hooks.md +119 −48
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
23 20
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,
2623 so one hook can’t prevent another matching hook from starting. so one hook cannot prevent another matching hook from starting.
27- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and24- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and
28 `Stop` run at turn scope.25 `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:Installed 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.
38
39In 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`
45
46If more than one hook source exists, Codex loads all matching 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.
39 50
4051If more than one `hooks.json` file exists, Codex loads all matching hooks.Project-local hooks load only when the project `.codex/` layer is trusted. In
4152Higher-precedence config layers don’t replace lower-precedence hooks.untrusted projects, Codex still loads user and system hooks from their own
53active config layers.
42 54
43## Config shape55## Config shape
44 56
127Notes:139Notes:
128 140
129- `timeout` is in seconds.141- `timeout` is in seconds.
130- `timeoutSec` is also accepted as an alias.
131- If `timeout` is omitted, Codex uses `600` seconds.142- If `timeout` is omitted, Codex uses `600` seconds.
132- `statusMessage` is optional.143- `statusMessage` is optional.
133- Commands run with the session `cwd` as their working directory.144- Commands run with the session `cwd` as their working directory.
135 relative path such as `.codex/hooks/...`. Codex may be started from a146 relative path such as `.codex/hooks/...`. Codex may be started from a
136 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.
137 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
138## Matcher patterns207## Matcher patterns
139 208
140The `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 `"*"`,
145 214
146| Event | What `matcher` filters | Notes |215| Event | What `matcher` filters | Notes |
147| --- | --- | --- |216| --- | --- | --- |
148217| `PermissionRequest` | tool name | Current Codex runtime only emits `Bash`. || `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
149218| `PostToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
150219| `PreToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
151220| `SessionStart` | start source | Current runtime values are `startup` and `resume`. || `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
152221| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event. || `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
153222| `Stop` | not supported | Any configured `matcher` is ignored for this event. || `Stop` | not supported | Any configured `matcher` is ignored for this event |
223
224\*For `apply_patch`, matchers can also use `Edit` or `Write`.
154 225
155Examples:226Examples:
156 227
157- `Bash`228- `Bash`
158229- `startup|resume`- `^apply_patch$`
159- `Edit|Write`230- `Edit|Write`
160231 - `mcp__filesystem__read_file`
161232That last example is still a valid regex, but current Codex `PreToolUse` and- `mcp__filesystem__.*`
162233`PostToolUse` events only emit `Bash`, so it won’t match anything today.- `startup|resume|clear`
163 234
164## Common input fields235## Common input fields
165 236
238 309
239### PreToolUse310### PreToolUse
240 311
241312Work in progress`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
242313 and MCP tool calls. It is still a guardrail rather than a complete enforcement
243314Currently `PreToolUse` only supports Bash tool interception. The model canboundary because Codex can often perform equivalent work through another
244315still work around this by writing its own script to disk and then running thatsupported tool path.
245script with Bash, so treat this as a useful guardrail rather than a complete
246enforcement boundary
247 316
248This doesn't intercept all shell calls yet, only the simple ones. The newer317This doesn't intercept all shell calls yet, only the simple ones. The newer
249 `unified_exec` mechanism allows richer streaming stdin/stdout handling of318 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
250319shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
251320Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
252 321
253322`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"`.
254 325
255Fields in addition to [Common input fields](#common-input-fields):326Fields in addition to [Common input fields](#common-input-fields):
256 327
257| Field | Type | Meaning |328| Field | Type | Meaning |
258| --- | --- | --- |329| --- | --- | --- |
259| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |330| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
260331| `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` |
261| `tool_use_id` | `string` | Tool-call id for this invocation |332| `tool_use_id` | `string` | Tool-call id for this invocation |
262333| `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. |
263 334
264Plain text on `stdout` is ignored.335Plain text on `stdout` is ignored.
265 336
293 364
294### PermissionRequest365### PermissionRequest
295 366
296Work in progress
297
298`PermissionRequest` runs when Codex is about to ask for approval, such as a367`PermissionRequest` runs when Codex is about to ask for approval, such as a
299shell escalation or managed-network approval. It can allow the request, deny368shell escalation or managed-network approval. It can allow the request, deny
300the request, or decline to decide and let the normal approval prompt continue.369the request, or decline to decide and let the normal approval prompt continue.
301It doesn't run for commands that don't need approval.370It doesn't run for commands that don't need approval.
302 371
303372`matcher` is applied to `tool_name`, which currently always equals `Bash`.`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`.
304 375
305Fields in addition to [Common input fields](#common-input-fields):376Fields in addition to [Common input fields](#common-input-fields):
306 377
307| Field | Type | Meaning |378| Field | Type | Meaning |
308| --- | --- | --- |379| --- | --- | --- |
309| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |380| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
310381| `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` |
311382| `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. |
312| `tool_input.description` | `string | null` | Human-readable approval reason, when Codex has one |383| `tool_input.description` | `string | null` | Human-readable approval reason, when Codex has one |
313 384
314Plain text on `stdout` is ignored.385Plain text on `stdout` is ignored.
350 421
351### PostToolUse422### PostToolUse
352 423
353424Work in progress`PostToolUse` runs after supported tools produce output, including Bash,
354425 `apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
355426Currently `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
356427commands that exit successfully: non-interactive `exec_command` calls can stillalready ran.
357trigger `PostToolUse` when Codex emits a Bash post-tool payload. It can’t undo
358side effects from the command that already ran.
359 428
360This doesn't intercept all shell calls yet, only the simple ones. The newer429This doesn't intercept all shell calls yet, only the simple ones. The newer
361 `unified_exec` mechanism allows richer streaming stdin/stdout handling of430 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
362431shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
363432Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
364 433
365434`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"`.
366 437
367Fields in addition to [Common input fields](#common-input-fields):438Fields in addition to [Common input fields](#common-input-fields):
368 439
369| Field | Type | Meaning |440| Field | Type | Meaning |
370| --- | --- | --- |441| --- | --- | --- |
371| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |442| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
372443| `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` |
373| `tool_use_id` | `string` | Tool-call id for this invocation |444| `tool_use_id` | `string` | Tool-call id for this invocation |
374445| `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. |
375446| `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. |
376 447
377Plain text on `stdout` is ignored.448Plain text on `stdout` is ignored.
378 449