hooks.md +130 −33
9- Run a custom validation check when a conversation turn stops, enforcing standards9- Run a custom validation check when a conversation turn stops, enforcing standards
10- Customize prompting when in a certain directory10- Customize prompting when in a certain directory
11 11
1212Hooks are behind a feature flag in `config.toml`:Hooks are enabled by default. If you need to turn them off in `config.toml`,
13set:
13 14
14```toml15```toml
15[features]16[features]
1617codex_hooks = truehooks = false
17```18```
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
19Runtime behavior to keep in mind:26Runtime behavior to keep in mind:
20 27
21- Matching hooks from multiple files all run.28- Matching hooks from multiple files all run.
22- Multiple matching command hooks for the same event are launched concurrently,29- Multiple matching command hooks for the same event are launched concurrently,
23 so one hook cannot prevent another matching hook from starting.30 so one hook cannot prevent another matching hook from starting.
31- Non-managed command hooks must be reviewed and trusted before they run.
24- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and32- `PreToolUse`, `PermissionRequest`, `PostToolUse`, `UserPromptSubmit`, and
25 `Stop` run at turn scope.33 `Stop` run at turn scope.
26 34
31- `hooks.json`39- `hooks.json`
32- inline `[hooks]` tables inside `config.toml`40- inline `[hooks]` tables inside `config.toml`
33 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
34In practice, the four most useful locations are:47In practice, the four most useful locations are:
35 48
36- `~/.codex/hooks.json`49- `~/.codex/hooks.json`
39- `<repo>/.codex/config.toml`52- `<repo>/.codex/config.toml`
40 53
41If more than one hook source exists, Codex loads all matching hooks.54If more than one hook source exists, Codex loads all matching hooks.
4255Higher-precedence config layers do not replace lower-precedence hooks.Higher-precedence config layers don't replace lower-precedence hooks.
43If a single layer contains both `hooks.json` and inline `[hooks]`, Codex56If a single layer contains both `hooks.json` and inline `[hooks]`, Codex
44merges them and warns at startup. Prefer one representation per layer.57merges them and warns at startup. Prefer one representation per layer.
45 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
46Project-local hooks load only when the project `.codex/` layer is trusted. In63Project-local hooks load only when the project `.codex/` layer is trusted. In
47untrusted projects, Codex still loads user and system hooks from their own64untrusted projects, Codex still loads user and system hooks from their own
48active config layers.65active config layers.
49 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
50## Config shape77## Config shape
51 78
52Hooks are organized in three levels:79Hooks are organized in three levels:
136- `timeout` is in seconds.163- `timeout` is in seconds.
137- If `timeout` is omitted, Codex uses `600` seconds.164- If `timeout` is omitted, Codex uses `600` seconds.
138- `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.
139- Commands run with the session `cwd` as their working directory.170- Commands run with the session `cwd` as their working directory.
140- 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
141 relative path such as `.codex/hooks/...`. Codex may be started from a172 relative path such as `.codex/hooks/...`. Codex may be started from a
144Equivalent inline TOML in `config.toml`:175Equivalent inline TOML in `config.toml`:
145 176
146```toml177```toml
147[features]
148codex_hooks = true
149
150[[hooks.PreToolUse]]178[[hooks.PreToolUse]]
151matcher = "^Bash$"179matcher = "^Bash$"
152 180
171Enterprise-managed requirements can also define hooks inline under `[hooks]`.199Enterprise-managed requirements can also define hooks inline under `[hooks]`.
172This is useful when admins want to enforce the hook configuration while200This is useful when admins want to enforce the hook configuration while
173delivering the actual scripts through MDM or another device-management system.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]`.
174 204
175```toml205```toml
176[features]206[features]
177207codex_hooks = truehooks = true
178 208
179[hooks]209[hooks]
180managed_dir = "/enterprise/hooks"210managed_dir = "/enterprise/hooks"
194 224
195- `managed_dir` is used on macOS and Linux.225- `managed_dir` is used on macOS and Linux.
196- `windows_managed_dir` is used on Windows.226- `windows_managed_dir` is used on Windows.
197227- Codex does not distribute the scripts in `managed_dir`; your enterprise- Codex doesn't distribute the scripts in `managed_dir`; your enterprise
198 tooling must install and update them separately.228 tooling must install and update them separately.
199- Managed hook commands should use absolute script paths under the configured229- Managed hook commands should use absolute script paths under the configured
200 managed directory.230 managed directory.
201 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
202## Matcher patterns272## Matcher patterns
203 273
204The `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 `"*"`,
208Only some current Codex events honor `matcher`:278Only some current Codex events honor `matcher`:
209 279
210| Event | What `matcher` filters | Notes |280| Event | What `matcher` filters | Notes |
211281| --- | --- | --- || ------------------- | ---------------------- | ------------------------------------------------------------ |
212| `PermissionRequest` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |282| `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 |283| `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 |284| `PreToolUse` | tool name | Support includes `Bash`, `apply_patch`\*, and MCP tool names |
216| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |286| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event |
217| `Stop` | not supported | Any configured `matcher` is ignored for this event |287| `Stop` | not supported | Any configured `matcher` is ignored for this event |
218 288
219289\*For `apply_patch`, matchers can also use `Edit` or `Write`.\*For `apply_patch`, `matcher` values can also use `Edit` or `Write`.
220 290
221Examples:291Examples:
222 292
234These are the shared fields you will usually use:304These are the shared fields you will usually use:
235 305
236| Field | Type | Meaning |306| Field | Type | Meaning |
237307| --- | --- | --- || ----------------- | ---------------- | ------------------------------------------- |
238| `session_id` | `string` | Current session or thread id. |308| `session_id` | `string` | Current session or thread id. |
239309| `transcript_path` | `string | null` | Path to the session transcript file, if any || `transcript_path` | `string \| null` | Path to the session transcript file, if any |
240| `cwd` | `string` | Working directory for the session |310| `cwd` | `string` | Working directory for the session |
241| `hook_event_name` | `string` | Current hook event name |311| `hook_event_name` | `string` | Current hook event name |
242312| `model` | `string` | Active model slug || `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`.
243 321
244322Turn-scoped hooks list `turn_id` in their event-specific tables.`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.
245 324
246If you need the full wire format, see [Schemas](#schemas).325If you need the full wire format, see [Schemas](#schemas).
247 326
283Fields in addition to [Common input fields](#common-input-fields):362Fields in addition to [Common input fields](#common-input-fields):
284 363
285| Field | Type | Meaning |364| Field | Type | Meaning |
286365| --- | --- | --- || -------- | -------- | -------------------------------------------------------- |
287366| `source` | `string` | How the session started: `startup` or `resume` || `source` | `string` | How the session started: `startup`, `resume`, or `clear` |
288 367
289Plain text on `stdout` is added as extra developer context.368Plain text on `stdout` is added as extra developer context.
290 369
305### PreToolUse384### PreToolUse
306 385
307`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,386`PreToolUse` can intercept Bash, file edits performed through `apply_patch`,
308387and MCP tool calls. It is still a guardrail rather than a complete enforcementand MCP tool calls. It's still a guardrail rather than a complete enforcement
309boundary because Codex can often perform equivalent work through another388boundary because Codex can often perform equivalent work through another
310supported tool path.389supported tool path.
311 390
315 `WebSearch` or other non-shell, non-MCP tool calls.394 `WebSearch` or other non-shell, non-MCP tool calls.
316 395
317`matcher` is applied to `tool_name` and matcher aliases. For file edits through396`matcher` is applied to `tool_name` and matcher aliases. For file edits through
318397`apply_patch`, matchers can use `apply_patch`, `Edit`, or `Write`; hook input`apply_patch`, `matcher` values can use `apply_patch`, `Edit`, or `Write`; hook input
319still reports `tool_name: "apply_patch"`.398still reports `tool_name: "apply_patch"`.
320 399
321Fields in addition to [Common input fields](#common-input-fields):400Fields in addition to [Common input fields](#common-input-fields):
322 401
323| Field | Type | Meaning |402| Field | Type | Meaning |
324403| --- | --- | --- || ------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
325| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |404| `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` |405| `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 |406| `tool_use_id` | `string` | Tool-call id for this invocation |
328407| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. || `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all arguments. |
329 408
330Plain text on `stdout` is ignored.409Plain text on `stdout` is ignored.
331 410
332411JSON 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
333412hook-specific shape:this hook-specific shape:
334 413
335```json414```json
336{415{
353 432
354You 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`.
355 434
356435`permissionDecision: "allow"` and `"ask"`, legacy `decision: "approve"`,To add model-visible context without blocking, return
357436`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and`hookSpecificOutput.additionalContext`:
358437`suppressOutput` are parsed but not supported yet, so they fail open.
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.
359 450
360### PermissionRequest451### PermissionRequest
361 452
371Fields in addition to [Common input fields](#common-input-fields):462Fields in addition to [Common input fields](#common-input-fields):
372 463
373| Field | Type | Meaning |464| Field | Type | Meaning |
374465| --- | --- | --- || ------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------- |
375| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |466| `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` |467| `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. |468| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. |
378469| `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 |
379 470
380Plain text on `stdout` is ignored.471Plain text on `stdout` is ignored.
381 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
382To approve the request, return:476To approve the request, return:
383 477
384```json478```json
427 `WebSearch` or other non-shell, non-MCP tool calls.521 `WebSearch` or other non-shell, non-MCP tool calls.
428 522
429`matcher` is applied to `tool_name` and matcher aliases. For file edits through523`matcher` is applied to `tool_name` and matcher aliases. For file edits through
430524`apply_patch`, matchers can use `apply_patch`, `Edit`, or `Write`; hook input`apply_patch`, `matcher` values can use `apply_patch`, `Edit`, or `Write`; hook input
431still reports `tool_name: "apply_patch"`.525still reports `tool_name: "apply_patch"`.
432 526
433Fields in addition to [Common input fields](#common-input-fields):527Fields in addition to [Common input fields](#common-input-fields):
434 528
435| Field | Type | Meaning |529| Field | Type | Meaning |
436530| --- | --- | --- || --------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
437| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |531| `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` |532| `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 |533| `tool_use_id` | `string` | Tool-call id for this invocation |
440534| `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all the args. || `tool_input` | `JSON value` | Tool-specific input. `Bash` and `apply_patch` use `tool_input.command` while MCP tools send all arguments. |
441| `tool_response` | `JSON value` | Tool-specific output. For MCP tools, this is the MCP call result. |535| `tool_response` | `JSON value` | Tool-specific output. For MCP tools, this is the MCP call result. |
442 536
443Plain text on `stdout` is ignored.537Plain text on `stdout` is ignored.
477Fields in addition to [Common input fields](#common-input-fields):571Fields in addition to [Common input fields](#common-input-fields):
478 572
479| Field | Type | Meaning |573| Field | Type | Meaning |
480574| --- | --- | --- || --------- | -------- | ---------------------------------------------- |
481| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |575| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
482| `prompt` | `string` | User prompt that's about to be sent |576| `prompt` | `string` | User prompt that's about to be sent |
483 577
515Fields in addition to [Common input fields](#common-input-fields):609Fields in addition to [Common input fields](#common-input-fields):
516 610
517| Field | Type | Meaning |611| Field | Type | Meaning |
518612| --- | --- | --- || ------------------------ | ---------------- | ------------------------------------------------- |
519| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |613| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
520| `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` |
521615| `last_assistant_message` | `string | null` | Latest assistant message text, if available || `last_assistant_message` | `string \| null` | Latest assistant message text, if available |
522 616
523`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
524for this event.618for this event.
544 638
545## Schemas639## Schemas
546 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
547If 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
548[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).