hooks.md +232 −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.
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.
39 66
4067If more than one `hooks.json` file exists, Codex loads all matching hooks.## Review and manage hooks
4168Higher-precedence config layers don’t replace lower-precedence hooks.
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.
42 76
43## Config shape77## Config shape
44 78
127Notes:161Notes:
128 162
129- `timeout` is in seconds.163- `timeout` is in seconds.
130- `timeoutSec` is also accepted as an alias.
131- If `timeout` is omitted, Codex uses `600` seconds.164- If `timeout` is omitted, Codex uses `600` seconds.
132- `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.
133- Commands run with the session `cwd` as their working directory.170- Commands run with the session `cwd` as their working directory.
134- 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
135 relative path such as `.codex/hooks/...`. Codex may be started from a172 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.173 subdirectory, and a git-root-based path keeps the hook location stable.
137 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
138## Matcher patterns272## Matcher patterns
139 273
140The `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 `"*"`,
144Only some current Codex events honor `matcher`:278Only some current Codex events honor `matcher`:
145 279
146| Event | What `matcher` filters | Notes |280| Event | What `matcher` filters | Notes |
147281| --- | --- | --- || ------------------- | ---------------------- | ------------------------------------------------------------ |
148282| `PermissionRequest` | tool name | Current Codex runtime only emits `Bash`. || `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
149283| `PostToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PostToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
150284| `PreToolUse` | tool name | Current Codex runtime only emits `Bash`. || `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
151285| `SessionStart` | start source | Current runtime values are `startup` and `resume`. || `SessionStart` | start source | Current runtime values are `startup`, `resume`, and `clear` |
152286| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event. || `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
153287| `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`.
154 290
155Examples:291Examples:
156 292
157- `Bash`293- `Bash`
158294- `startup|resume`- `^apply_patch$`
159- `Edit|Write`295- `Edit|Write`
160296 - `mcp__filesystem__read_file`
161297That last example is still a valid regex, but current Codex `PreToolUse` and- `mcp__filesystem__.*`
162298`PostToolUse` events only emit `Bash`, so it won’t match anything today.- `startup|resume|clear`
163 299
164## Common input fields300## Common input fields
165 301
168These are the shared fields you will usually use:304These are the shared fields you will usually use:
169 305
170| Field | Type | Meaning |306| Field | Type | Meaning |
171307| --- | --- | --- || ----------------- | ---------------- | ------------------------------------------- |
172| `session_id` | `string` | Current session or thread id. |308| `session_id` | `string` | Current session or thread id. |
173309| `transcript_path` | `string | null` | Path to the session transcript file, if any || `transcript_path` | `string \| null` | Path to the session transcript file, if any |
174| `cwd` | `string` | Working directory for the session |310| `cwd` | `string` | Working directory for the session |
175| `hook_event_name` | `string` | Current hook event name |311| `hook_event_name` | `string` | Current hook event name |
176312| `model` | `string` | Active model slug || `model` | `string` | Codex-specific extension. Active model slug |
177 313
178314Turn-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.
179 324
180If you need the full wire format, see [Schemas](#schemas).325If you need the full wire format, see [Schemas](#schemas).
181 326
217Fields in addition to [Common input fields](#common-input-fields):362Fields in addition to [Common input fields](#common-input-fields):
218 363
219| Field | Type | Meaning |364| Field | Type | Meaning |
220365| --- | --- | --- || -------- | -------- | -------------------------------------------------------- |
221366| `source` | `string` | How the session started: `startup` or `resume` || `source` | `string` | How the session started: `startup`, `resume`, or `clear` |
222 367
223Plain text on `stdout` is added as extra developer context.368Plain text on `stdout` is added as extra developer context.
224 369
238 383
239### PreToolUse384### PreToolUse
240 385
241386Work in progress`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
242387 and MCP tool calls. It's still a guardrail rather than a complete enforcement
243388Currently `PreToolUse` only supports Bash tool interception. The model canboundary because Codex can often perform equivalent work through another
244389still 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 390
248This 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
249 `unified_exec` mechanism allows richer streaming stdin/stdout handling of392 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
250393shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
251394Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
252 395
253396`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"`.
254 399
255Fields in addition to [Common input fields](#common-input-fields):400Fields in addition to [Common input fields](#common-input-fields):
256 401
257| Field | Type | Meaning |402| Field | Type | Meaning |
258403| --- | --- | --- || ------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
259| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |404| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
260405| `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 |406| `tool_use_id` | `string` | Tool-call id for this invocation |
262407| `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. |
263 408
264Plain text on `stdout` is ignored.409Plain text on `stdout` is ignored.
265 410
266411JSON 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
267412hook-specific shape:this hook-specific shape:
268 413
269```json414```json
270{415{
287 432
288You 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`.
289 434
290435`permissionDecision: "allow"` and `"ask"`, legacy `decision: "approve"`,To add model-visible context without blocking, return
291436`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and`hookSpecificOutput.additionalContext`:
292`suppressOutput` are parsed but not supported yet, so they fail open.
293 437
294438### PermissionRequest```json
439{
440 "hookSpecificOutput": {
441 "hookEventName": "PreToolUse",
442 "additionalContext": "The pending command touches generated files."
443 }
444}
445```
295 446
296447Work 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
297 452
298`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
299shell escalation or managed-network approval. It can allow the request, deny454shell escalation or managed-network approval. It can allow the request, deny
300the 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.
301It doesn't run for commands that don't need approval.456It doesn't run for commands that don't need approval.
302 457
303458`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`.
304 461
305Fields in addition to [Common input fields](#common-input-fields):462Fields in addition to [Common input fields](#common-input-fields):
306 463
307| Field | Type | Meaning |464| Field | Type | Meaning |
308465| --- | --- | --- || ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- |
309| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |466| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
310467| `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` |
311468| `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. |
312469| `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 |
313 470
314Plain text on `stdout` is ignored.471Plain text on `stdout` is ignored.
315 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
316To approve the request, return:476To approve the request, return:
317 477
318```json478```json
350 510
351### PostToolUse511### PostToolUse
352 512
353513Work in progress`PostToolUse` runs after supported tools produce output, including Bash,
354514 `apply_patch`, and MCP tool calls. For Bash, it also runs after commands that
355515Currently `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
356516commands 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 517
360This 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
361 `unified_exec` mechanism allows richer streaming stdin/stdout handling of519 `unified_exec` mechanism allows richer streaming stdin/stdout handling of
362520shell, but interception is incomplete. Similarly, this doesn’t intercept MCP, shell, but interception is incomplete. Similarly, this doesn't intercept
363521Write, WebSearch, or other non-shell tool calls. `WebSearch` or other non-shell, non-MCP tool calls.
364 522
365523`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"`.
366 526
367Fields in addition to [Common input fields](#common-input-fields):527Fields in addition to [Common input fields](#common-input-fields):
368 528
369| Field | Type | Meaning |529| Field | Type | Meaning |
370530| --- | --- | --- || --------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
371| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |531| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
372532| `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 |533| `tool_use_id` | `string` | Tool-call id for this invocation |
374534| `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. |
375535| `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 536
377Plain text on `stdout` is ignored.537Plain text on `stdout` is ignored.
378 538
411Fields in addition to [Common input fields](#common-input-fields):571Fields in addition to [Common input fields](#common-input-fields):
412 572
413| Field | Type | Meaning |573| Field | Type | Meaning |
414574| --- | --- | --- || --------- | -------- | ---------------------------------------------- |
415| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |575| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
416| `prompt` | `string` | User prompt that's about to be sent |576| `prompt` | `string` | User prompt that's about to be sent |
417 577
449Fields in addition to [Common input fields](#common-input-fields):609Fields in addition to [Common input fields](#common-input-fields):
450 610
451| Field | Type | Meaning |611| Field | Type | Meaning |
452612| --- | --- | --- || ------------------------ | ---------------- | ------------------------------------------------- |
453| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |613| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
454| `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` |
455615| `last_assistant_message` | `string | null` | Latest assistant message text, if available || `last_assistant_message` | `string \| null` | Latest assistant message text, if available |
456 616
457`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
458for this event.618for this event.
478 638
479## Schemas639## Schemas
480 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
481If 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
482[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).