hooks.md +398 −0 added
1# Hooks
2
3Experimental. Hooks are under active development. Windows support temporarily
4disabled.
5
6Hooks are an extensibility framework for Codex. They allow
7you to inject your own scripts into the agentic loop, enabling features such as:
8
9- Send the conversation to a custom logging/analytics engine
10- Scan your team's prompts to block accidentally pasting API keys
11- Summarize conversations to create persistent memories automatically
12- Run a custom validator when a conversation turn stops, enforcing standards
13- Customize prompting when in a certain directory
14
15Hooks are behind a feature flag in `config.toml`:
16
17```toml
18[features]
19codex_hooks = true
20```
21
22Runtime behavior to keep in mind:
23
24- Matching hooks from multiple files all run.
25- Multiple matching command hooks for the same event are launched concurrently,
26 so one hook cannot prevent another matching hook from starting.
27- `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, and `Stop` run at turn
28 scope.
29- Hooks are currently disabled on Windows.
30
31## Where Codex looks for hooks
32
33Codex discovers `hooks.json` next to active config layers.
34
35In practice, the two most useful locations are:
36
37- `~/.codex/hooks.json`
38- `<repo>/.codex/hooks.json`
39
40If more than one `hooks.json` file exists, Codex loads all matching hooks.
41Higher-precedence config layers do not replace lower-precedence hooks.
42
43## Config shape
44
45Hooks are organized in three levels:
46
47- A hook event such as `PreToolUse`, `PostToolUse`, or `Stop`
48- A matcher group that decides when that event matches
49- One or more hook handlers that run when the matcher group matches
50
51```json
52{
53 "hooks": {
54 "SessionStart": [
55 {
56 "matcher": "startup|resume",
57 "hooks": [
58 {
59 "type": "command",
60 "command": "python3 ~/.codex/hooks/session_start.py",
61 "statusMessage": "Loading session notes"
62 }
63 ]
64 }
65 ],
66 "PreToolUse": [
67 {
68 "matcher": "Bash",
69 "hooks": [
70 {
71 "type": "command",
72 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"",
73 "statusMessage": "Checking Bash command"
74 }
75 ]
76 }
77 ],
78 "PostToolUse": [
79 {
80 "matcher": "Bash",
81 "hooks": [
82 {
83 "type": "command",
84 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py\"",
85 "statusMessage": "Reviewing Bash output"
86 }
87 ]
88 }
89 ],
90 "UserPromptSubmit": [
91 {
92 "hooks": [
93 {
94 "type": "command",
95 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/user_prompt_submit_data_flywheel.py\""
96 }
97 ]
98 }
99 ],
100 "Stop": [
101 {
102 "hooks": [
103 {
104 "type": "command",
105 "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_continue.py\"",
106 "timeout": 30
107 }
108 ]
109 }
110 ]
111 }
112}
113```
114
115Notes:
116
117- `timeout` is in seconds.
118- `timeoutSec` is also accepted as an alias.
119- If `timeout` is omitted, Codex uses `600` seconds.
120- `statusMessage` is optional.
121- Commands run with the session `cwd` as their working directory.
122- For repo-local hooks, prefer resolving from the git root instead of using a
123 relative path such as `.codex/hooks/...`. Codex may be started from a
124 subdirectory, and a git-root-based path keeps the hook location stable.
125
126## Matcher patterns
127
128The `matcher` field is a regex string that filters when hooks fire. Use `"*"`,
129`""`, or omit `matcher` entirely to match every occurrence of a supported
130event.
131
132Only some current Codex events honor `matcher`:
133
134| Event | What `matcher` filters | Notes |
135| --- | --- | --- |
136| `PostToolUse` | tool name | Current Codex runtime only emits `Bash`. |
137| `PreToolUse` | tool name | Current Codex runtime only emits `Bash`. |
138| `SessionStart` | start source | Current runtime values are `startup` and `resume`. |
139| `UserPromptSubmit` | not supported | Any configured `matcher` is ignored for this event. |
140| `Stop` | not supported | Any configured `matcher` is ignored for this event. |
141
142Examples:
143
144- `Bash`
145- `startup|resume`
146- `Edit|Write`
147
148That last example is still a valid regex, but current Codex `PreToolUse` and
149`PostToolUse` events only emit `Bash`, so it will not match anything today.
150
151## Common input fields
152
153Every command hook receives one JSON object on `stdin`.
154
155These are the shared fields you will usually use:
156
157| Field | Type | Meaning |
158| --- | --- | --- |
159| `session_id` | `string` | Current session or thread id. |
160| `transcript_path` | `string | null` | Path to the session transcript file, if any |
161| `cwd` | `string` | Working directory for the session |
162| `hook_event_name` | `string` | Current hook event name |
163| `model` | `string` | Active model slug |
164
165Turn-scoped hooks list `turn_id` in their event-specific tables.
166
167If you need the full wire format, see [Schemas](#schemas).
168
169## Common output fields
170
171`SessionStart`, `UserPromptSubmit`, and `Stop` support these shared JSON
172fields:
173
174```json
175{
176 "continue": true,
177 "stopReason": "optional",
178 "systemMessage": "optional",
179 "suppressOutput": false
180}
181```
182
183| Field | Effect |
184| ---------------- | ----------------------------------------------- |
185| `continue` | If `false`, marks that hook run as stopped |
186| `stopReason` | Recorded as the reason for stopping |
187| `systemMessage` | Surfaced as a warning in the UI or event stream |
188| `suppressOutput` | Parsed today but not yet implemented |
189
190Exit `0` with no output is treated as success and Codex continues.
191
192`PreToolUse` supports `systemMessage`, but `continue`, `stopReason`, and
193`suppressOutput` are not currently supported for that event.
194
195`PostToolUse` supports `systemMessage`, `continue: false`, and `stopReason`.
196`suppressOutput` is parsed but not currently supported for that event.
197
198## Hooks
199
200### SessionStart
201
202`matcher` is applied to `source` for this event.
203
204Fields in addition to [Common input fields](#common-input-fields):
205
206| Field | Type | Meaning |
207| --- | --- | --- |
208| `source` | `string` | How the session started: `startup` or `resume` |
209
210Plain text on `stdout` is added as extra developer context.
211
212JSON on `stdout` supports [Common output fields](#common-output-fields) and this
213hook-specific shape:
214
215```json
216{
217 "hookSpecificOutput": {
218 "hookEventName": "SessionStart",
219 "additionalContext": "Load the workspace conventions before editing."
220 }
221}
222```
223
224That `additionalContext` text is added as extra developer context.
225
226### PreToolUse
227
228Currently `PreToolUse` only supports Bash tool interception. The model can
229still work around this by writing its own script to disk and then running that
230script with Bash, so treat this as a useful guardrail rather than a complete
231enforcement boundary.
232
233`matcher` is applied to `tool_name`, which currently always equals `Bash`.
234
235Fields in addition to [Common input fields](#common-input-fields):
236
237| Field | Type | Meaning |
238| --- | --- | --- |
239| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
240| `tool_name` | `string` | Currently always `Bash` |
241| `tool_use_id` | `string` | Tool-call id for this invocation |
242| `tool_input.command` | `string` | Shell command Codex is about to run |
243
244Plain text on `stdout` is ignored.
245
246JSON on `stdout` can use `systemMessage` and can block a Bash command with this
247hook-specific shape:
248
249```json
250{
251 "hookSpecificOutput": {
252 "hookEventName": "PreToolUse",
253 "permissionDecision": "deny",
254 "permissionDecisionReason": "Destructive command blocked by hook."
255 }
256}
257```
258
259Codex also accepts this older block shape:
260
261```json
262{
263 "decision": "block",
264 "reason": "Destructive command blocked by hook."
265}
266```
267
268You can also use exit code `2` and write the blocking reason to `stderr`.
269
270`permissionDecision: "allow"` and `"ask"`, legacy `decision: "approve"`,
271`updatedInput`, `additionalContext`, `continue: false`, `stopReason`, and
272`suppressOutput` are parsed but not supported yet, so they fail open.
273
274### PostToolUse
275
276Currently `PostToolUse` only supports Bash tool results. It is not limited to
277commands that exit successfully: non-interactive `exec_command` calls can still
278trigger `PostToolUse` when Codex emits a Bash post-tool payload. It cannot undo
279side effects from the command that already ran.
280
281`matcher` is applied to `tool_name`, which currently always equals `Bash`.
282
283Fields in addition to [Common input fields](#common-input-fields):
284
285| Field | Type | Meaning |
286| --- | --- | --- |
287| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
288| `tool_name` | `string` | Currently always `Bash` |
289| `tool_use_id` | `string` | Tool-call id for this invocation |
290| `tool_input.command` | `string` | Shell command Codex just ran |
291| `tool_response` | `JSON value` | Bash tool output payload. Today this is usually a JSON string |
292
293Plain text on `stdout` is ignored.
294
295JSON on `stdout` can use `systemMessage` and this hook-specific shape:
296
297```json
298{
299 "decision": "block",
300 "reason": "The Bash output needs review before continuing.",
301 "hookSpecificOutput": {
302 "hookEventName": "PostToolUse",
303 "additionalContext": "The command updated generated files."
304 }
305}
306```
307
308That `additionalContext` text is added as extra developer context.
309
310For this event, `decision: "block"` does not undo the completed Bash command.
311Instead, Codex records the feedback, replaces the tool result with that
312feedback, and continues the model from the hook-provided message.
313
314You can also use exit code `2` and write the feedback reason to `stderr`.
315
316To stop normal processing of the original tool result after the command has
317already run, return `continue: false`. Codex will replace the tool result with
318your feedback or stop text and continue from there.
319
320`updatedMCPToolOutput` and `suppressOutput` are parsed but not supported yet,
321so they fail open.
322
323### UserPromptSubmit
324
325`matcher` is not currently used for this event.
326
327Fields in addition to [Common input fields](#common-input-fields):
328
329| Field | Type | Meaning |
330| --- | --- | --- |
331| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
332| `prompt` | `string` | User prompt that is about to be sent |
333
334Plain text on `stdout` is added as extra developer context.
335
336JSON on `stdout` supports [Common output fields](#common-output-fields) and
337this hook-specific shape:
338
339```json
340{
341 "hookSpecificOutput": {
342 "hookEventName": "UserPromptSubmit",
343 "additionalContext": "Ask for a clearer reproduction before editing files."
344 }
345}
346```
347
348That `additionalContext` text is added as extra developer context.
349
350To block the prompt, return:
351
352```json
353{
354 "decision": "block",
355 "reason": "Ask for confirmation before doing that."
356}
357```
358
359You can also use exit code `2` and write the blocking reason to `stderr`.
360
361### Stop
362
363`matcher` is not currently used for this event.
364
365Fields in addition to [Common input fields](#common-input-fields):
366
367| Field | Type | Meaning |
368| --- | --- | --- |
369| `turn_id` | `string` | Codex-specific extension. Active Codex turn id |
370| `stop_hook_active` | `boolean` | Whether this turn was already continued by `Stop` |
371| `last_assistant_message` | `string | null` | Latest assistant message text, if available |
372
373`Stop` expects JSON on `stdout` when it exits `0`. Plain text output is invalid
374for this event.
375
376JSON on `stdout` supports [Common output fields](#common-output-fields). To keep
377Codex going, return:
378
379```json
380{
381 "decision": "block",
382 "reason": "Run one more pass over the failing tests."
383}
384```
385
386You can also use exit code `2` and write the continuation reason to `stderr`.
387
388For this event, `decision: "block"` does not reject the turn. Instead, it tells
389Codex to continue and automatically creates a new continuation prompt that acts
390as a new user prompt, using your `reason` as that prompt text.
391
392If any matching `Stop` hook returns `continue: false`, that takes precedence
393over continuation decisions from other matching `Stop` hooks.
394
395## Schemas
396
397If you need the exact current wire format, see the generated schemas in the
398[Codex GitHub repository](https://github.com/openai/codex/tree/main/codex-rs/hooks/schema/generated).