This page is larger than the 256 KiB this site keeps, so one side of the diff below stops where the stored text does.
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 2
22
33> Reference for Claude Code hook events, configuration schema, JSON input/output formats, exit codes, async hooks, HTTP hooks, prompt hooks, and MCP tool hooks.
44
5<Tip>
6 For a quickstart guide with examples, see [Automate actions with hooks](/docs/en/hooks-guide).
7</Tip>
5> [!TIP]
6>
7> For a quickstart guide with examples, see [Automate actions with hooks](/docs/en/hooks-guide).
88
99Hooks are user-defined shell commands, HTTP endpoints, MCP tool calls, LLM prompts, or subagents that execute automatically at specific points in Claude Code's lifecycle. Claude Code fires the same hook events wherever it runs: sessions in the terminal, IDE extensions, the [Desktop app](/docs/en/desktop-quickstart), and [cloud sessions](/docs/en/claude-code-on-the-web). Use this reference to look up event schemas, configuration options, JSON input/output formats, and advanced features like async hooks, HTTP hooks, and MCP tool hooks.
1010
from line 20
2020* per turn: `UserPromptSubmit`, `Stop`, and `StopFailure`
2121* on every tool call inside the agentic loop: `PreToolUse` and `PostToolUse`, except [`EndConversation`](/docs/en/tools-reference#endconversation-tool-behavior) calls, which skip both
2222
23<div style={{maxWidth: "500px", margin: "0 auto"}}>
24 <Frame>
25 <img src="https://mintcdn.com/claude-code/x7pO8l4XcvAXCoVc/images/hooks-lifecycle.svg?fit=max&auto=format&n=x7pO8l4XcvAXCoVc&q=85&s=81b9256c1bbe8832553485f5d9e9c746" className="dark:hidden" alt="Hook lifecycle diagram showing optional Setup feeding into SessionStart, then a per-turn loop containing UserPromptSubmit, UserPromptExpansion for slash commands, the nested agentic loop (PreToolUse, PermissionRequest, PostToolUse, PostToolUseFailure, PostToolBatch, SubagentStart/Stop, TaskCreated, TaskCompleted), and Stop or StopFailure, followed by TeammateIdle, PreCompact, PostCompact, and SessionEnd, with Elicitation and ElicitationResult nested inside MCP tool execution, PermissionDenied as a side branch from PermissionRequest for auto-mode denials, WorktreeCreate, WorktreeRemove, Notification, ConfigChange, InstructionsLoaded, CwdChanged, FileChanged, and DirectoryAdded as standalone async events, PreModelSwitch as a standalone sequential event that runs before a requested model switch, PostModelSwitch as a standalone async event that runs after the session's model changes, and MessageDisplay as a display-only event that runs while assistant message text streams" width="520" height="1336" data-path="images/hooks-lifecycle.svg" />
26
27 <img src="https://mintcdn.com/claude-code/x7pO8l4XcvAXCoVc/images/hooks-lifecycle-dark.svg?fit=max&auto=format&n=x7pO8l4XcvAXCoVc&q=85&s=c9b3d88487335f58cce0b52e2f9e7531" className="hidden dark:block" alt="Hook lifecycle diagram showing optional Setup feeding into SessionStart, then a per-turn loop containing UserPromptSubmit, UserPromptExpansion for slash commands, the nested agentic loop (PreToolUse, PermissionRequest, PostToolUse, PostToolUseFailure, PostToolBatch, SubagentStart/Stop, TaskCreated, TaskCompleted), and Stop or StopFailure, followed by TeammateIdle, PreCompact, PostCompact, and SessionEnd, with Elicitation and ElicitationResult nested inside MCP tool execution, PermissionDenied as a side branch from PermissionRequest for auto-mode denials, WorktreeCreate, WorktreeRemove, Notification, ConfigChange, InstructionsLoaded, CwdChanged, FileChanged, and DirectoryAdded as standalone async events, PreModelSwitch as a standalone sequential event that runs before a requested model switch, PostModelSwitch as a standalone async event that runs after the session's model changes, and MessageDisplay as a display-only event that runs while assistant message text streams" width="520" height="1336" data-path="images/hooks-lifecycle-dark.svg" />
28 </Frame>
29</div>
23
3024
3125The table below summarizes when each event fires. The [Hook events](#hook-events) section documents the full input schema and decision control options for each one.
3226
from line 64
7064
7165To see how the event, the matcher, and the handler fit together, consider this `PreToolUse` hook that blocks destructive shell commands.
7266
73<Tabs>
74 <Tab title="macOS/Linux">
75 The `matcher` narrows to Bash tool calls and the `if` condition narrows further to Bash subcommands matching `rm *`, so `block-rm.sh` only spawns when both filters match:
76
77 ```json theme={null}
78 {
79 "hooks": {
80 "PreToolUse": [
67**macOS/Linux**
68
69The `matcher` narrows to Bash tool calls and the `if` condition narrows further to Bash subcommands matching `rm *`, so `block-rm.sh` only spawns when both filters match:
70
71```json theme={null}
72{
73 "hooks": {
74 "PreToolUse": [
75 {
76 "matcher": "Bash",
77 "hooks": [
8178 {
82 "matcher": "Bash",
83 "hooks": [
84 {
85 "type": "command",
86 "if": "Bash(rm *)",
87 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
88 "args": []
89 }
79 "type": "command",
80 "if": "Bash(rm *)",
81 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
82 "args": []
83 }
84 ]
85 }
86 ]
87 }
88}
89```
90
91The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf`. Save it to `.claude/hooks/block-rm.sh` in your project and make it executable with `chmod +x .claude/hooks/block-rm.sh` so Claude Code can run it:
92
93```bash theme={null}
94#!/bin/bash
95# .claude/hooks/block-rm.sh
96COMMAND=$(jq -r '.tool_input.command')
97
98if echo "$COMMAND" | grep -q 'rm -rf'; then
99 jq -n '{
100 hookSpecificOutput: {
101 hookEventName: "PreToolUse",
102 permissionDecision: "deny",
103 permissionDecisionReason: "Destructive command blocked by hook"
104 }
105 }'
106else
107 exit 0 # no decision; normal permission flow applies
108fi
109```
110
111This script, like the other Bash examples on this page that parse JSON input, uses `jq`, so install `jq` and make sure it is on your `PATH` before trying them.
112
113**Windows (PowerShell)**
114
115The matcher `Bash|PowerShell` covers the [PowerShell tool](#powershell) as well as Bash. A single `if` rule matches only one tool's calls, so each tool gets its own handler: the first narrows to Bash subcommands matching `rm *`, the second to PowerShell commands matching `Remove-Item *`. Both run the same script through `powershell.exe`:
116
117```json theme={null}
118{
119 "hooks": {
120 "PreToolUse": [
121 {
122 "matcher": "Bash|PowerShell",
123 "hooks": [
124 {
125 "type": "command",
126 "if": "Bash(rm *)",
127 "command": "powershell.exe",
128 "args": [
129 "-NoProfile",
130 "-ExecutionPolicy",
131 "Bypass",
132 "-File",
133 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
134 ]
135 },
136 {
137 "type": "command",
138 "if": "PowerShell(Remove-Item *)",
139 "command": "powershell.exe",
140 "args": [
141 "-NoProfile",
142 "-ExecutionPolicy",
143 "Bypass",
144 "-File",
145 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
90146 ]
91147 }
92148 ]
93149 }
150 ]
151 }
152}
153```
154
155The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
156
157The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf` or `Remove-Item` followed by `-Recurse`. Save it to `.claude/hooks/block-rm.ps1` in your project:
158
159```powershell theme={null}
160# .claude/hooks/block-rm.ps1
161$callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
162$command = $callInput.tool_input.command
163
164if ($command -match 'rm -rf|Remove-Item.*-Recurse') {
165 @{
166 hookSpecificOutput = @{
167 hookEventName = "PreToolUse"
168 permissionDecision = "deny"
169 permissionDecisionReason = "Destructive command blocked by hook"
94170 }
95 ```
96
97 The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf`. Save it to `.claude/hooks/block-rm.sh` in your project and make it executable with `chmod +x .claude/hooks/block-rm.sh` so Claude Code can run it:
98
99 ```bash theme={null}
100 #!/bin/bash
101 # .claude/hooks/block-rm.sh
102 COMMAND=$(jq -r '.tool_input.command')
103
104 if echo "$COMMAND" | grep -q 'rm -rf'; then
105 jq -n '{
106 hookSpecificOutput: {
107 hookEventName: "PreToolUse",
108 permissionDecision: "deny",
109 permissionDecisionReason: "Destructive command blocked by hook"
110 }
111 }'
112 else
113 exit 0 # no decision; normal permission flow applies
114 fi
115 ```
116
117 This script, like the other Bash examples on this page that parse JSON input, uses `jq`, so install `jq` and make sure it is on your `PATH` before trying them.
118 </Tab>
119
120 <Tab title="Windows (PowerShell)">
121 The matcher `Bash|PowerShell` covers the [PowerShell tool](#powershell) as well as Bash. A single `if` rule matches only one tool's calls, so each tool gets its own handler: the first narrows to Bash subcommands matching `rm *`, the second to PowerShell commands matching `Remove-Item *`. Both run the same script through `powershell.exe`:
122
123 ```json theme={null}
124 {
125 "hooks": {
126 "PreToolUse": [
127 {
128 "matcher": "Bash|PowerShell",
129 "hooks": [
130 {
131 "type": "command",
132 "if": "Bash(rm *)",
133 "command": "powershell.exe",
134 "args": [
135 "-NoProfile",
136 "-ExecutionPolicy",
137 "Bypass",
138 "-File",
139 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
140 ]
141 },
142 {
143 "type": "command",
144 "if": "PowerShell(Remove-Item *)",
145 "command": "powershell.exe",
146 "args": [
147 "-NoProfile",
148 "-ExecutionPolicy",
149 "Bypass",
150 "-File",
151 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
152 ]
153 }
154 ]
155 }
156 ]
157 }
158 }
159 ```
160
161 The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
162
163 The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf` or `Remove-Item` followed by `-Recurse`. Save it to `.claude/hooks/block-rm.ps1` in your project:
164
165 ```powershell theme={null}
166 # .claude/hooks/block-rm.ps1
167 $callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
168 $command = $callInput.tool_input.command
169
170 if ($command -match 'rm -rf|Remove-Item.*-Recurse') {
171 @{
172 hookSpecificOutput = @{
173 hookEventName = "PreToolUse"
174 permissionDecision = "deny"
175 permissionDecisionReason = "Destructive command blocked by hook"
176 }
177 } | ConvertTo-Json
178 } else {
179 exit 0 # no decision; normal permission flow applies
180 }
181 ```
182 </Tab>
183</Tabs>
171 } | ConvertTo-Json
172} else {
173 exit 0 # no decision; normal permission flow applies
174}
175```
184176
185177Now suppose Claude Code decides to run `Bash "rm -rf /tmp/build"` against the macOS/Linux config. Here's what happens:
186178
187<Frame>
188 <img src="https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/hook-resolution.svg?fit=max&auto=format&n=ikqp3_70mqIahteV&q=85&s=be0bf3053550c26de5f54cd64674c197" className="dark:hidden" alt="Diagram of hook resolution: PreToolUse fires, the matcher checks for a Bash match, then the if condition checks for a Bash(rm *) match. If both match, the hook command runs and returns permissionDecision deny, so the tool call is blocked and Claude Code continues. If either check fails to match, the hook is skipped and the tool call is allowed to proceed." width="930" height="270" data-path="images/hook-resolution.svg" />
189
190 <img src="https://mintcdn.com/claude-code/_xqph1dUOslCOwsj/images/hook-resolution-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=e80af91f8507cee6bd51ac3c2dd92f63" className="hidden dark:block" alt="Diagram of hook resolution: PreToolUse fires, the matcher checks for a Bash match, then the if condition checks for a Bash(rm *) match. If both match, the hook command runs and returns permissionDecision deny, so the tool call is blocked and Claude Code continues. If either check fails to match, the hook is skipped and the tool call is allowed to proceed." width="930" height="270" data-path="images/hook-resolution-dark.svg" />
191</Frame>
192
193<Steps>
194 <Step title="Event fires">
195 The `PreToolUse` event fires. Claude Code sends the tool input as JSON on stdin to the hook:
196
197 ```json theme={null}
198 { "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" }, ... }
199 ```
200 </Step>
201
202 <Step title="Matcher checks">
203 The matcher `"Bash"` matches the tool name, so this hook group activates. If you omit the matcher or use `"*"`, the group activates on every occurrence of the event.
204 </Step>
205
206 <Step title="If condition checks">
207 The `if` condition `"Bash(rm *)"` matches because `rm -rf /tmp/build` is a subcommand matching `rm *`, so this handler spawns. If the command had been `npm test`, the `if` check would fail and `block-rm.sh` would never run, avoiding the process spawn overhead. The `if` field is optional; without it, every handler in the matched group runs.
208 </Step>
209
210 <Step title="Hook handler runs">
211 The script inspects the full command and finds `rm -rf`, so it prints a decision to stdout:
212
213 ```json theme={null}
214 {
215 "hookSpecificOutput": {
216 "hookEventName": "PreToolUse",
217 "permissionDecision": "deny",
218 "permissionDecisionReason": "Destructive command blocked by hook"
219 }
220 }
221 ```
222
223 If the command had been a safer `rm` variant like `rm file.txt`, the script would hit `exit 0` instead. Exit code 0 with no output means the hook has no decision to report, so the tool call continues through the normal [permission flow](/docs/en/permissions). The hook can deny the call, but staying silent doesn't approve it.
224 </Step>
225
226 <Step title="Claude Code acts on the result">
227 Claude Code reads the JSON decision, blocks the tool call, and shows Claude the reason.
228 </Step>
229</Steps>
179
180
181**1. Event fires**
182
183The `PreToolUse` event fires. Claude Code sends the tool input as JSON on stdin to the hook:
184
185```json theme={null}
186{ "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" }, ... }
187```
188
189**2. Matcher checks**
190
191The matcher `"Bash"` matches the tool name, so this hook group activates. If you omit the matcher or use `"*"`, the group activates on every occurrence of the event.
192
193**3. If condition checks**
194
195The `if` condition `"Bash(rm *)"` matches because `rm -rf /tmp/build` is a subcommand matching `rm *`, so this handler spawns. If the command had been `npm test`, the `if` check would fail and `block-rm.sh` would never run, avoiding the process spawn overhead. The `if` field is optional; without it, every handler in the matched group runs.
196
197**4. Hook handler runs**
198
199The script inspects the full command and finds `rm -rf`, so it prints a decision to stdout:
200
201```json theme={null}
202{
203 "hookSpecificOutput": {
204 "hookEventName": "PreToolUse",
205 "permissionDecision": "deny",
206 "permissionDecisionReason": "Destructive command blocked by hook"
207 }
208}
209```
210
211If the command had been a safer `rm` variant like `rm file.txt`, the script would hit `exit 0` instead. Exit code 0 with no output means the hook has no decision to report, so the tool call continues through the normal [permission flow](/docs/en/permissions). The hook can deny the call, but staying silent doesn't approve it.
212
213**5. Claude Code acts on the result**
214
215Claude Code reads the JSON decision, blocks the tool call, and shows Claude the reason.
230216
231217The [Configuration](#configuration) section below documents the full schema, and each [hook event](#hook-events) section documents what input your command receives and what output it can return.
232218
from line 226
240226
241227See [How a hook resolves](#how-a-hook-resolves) above for a complete walkthrough with an annotated example.
242228
243<Note>
244 This page uses specific terms for each level: **hook event** for the lifecycle point, **matcher group** for the filter, and **hook handler** for the shell command, HTTP endpoint, MCP tool, prompt, or agent that runs. "Hook" on its own refers to the general feature.
245</Note>
229> [!NOTE]
230>
231> This page uses specific terms for each level: **hook event** for the lifecycle point, **matcher group** for the filter, and **hook handler** for the shell command, HTTP endpoint, MCP tool, prompt, or agent that runs. "Hook" on its own refers to the general feature.
246232
247233### Hook locations
248234
from line 415
429415
430416In an `if` condition for a file tool, a single-segment directory pattern like `"Edit(src/**)"` matches only the `src` directory in the working directory and the files under it. To match a directory named `src` at any depth, write `"Edit(**/src/**)"`.
431417
432<h4 id="bash-if-matching">
433 How `if` patterns match Bash commands
434</h4>
418#### How `if` patterns match Bash commands
435419
436420For Bash patterns in the [`if` field](#common-fields), whether your hook command runs depends on the shape of the pattern and the Bash command Claude is invoking. Leading `VAR=value` assignments are stripped before matching.
437421
from line 442
458442| `shell` | no | Shell to use for this hook. Accepts `"bash"` or `"powershell"`. Defaults to `"bash"`, or to `"powershell"` on Windows when Git Bash isn't installed. Setting `"powershell"` runs the command via PowerShell on Windows. Does not require `CLAUDE_CODE_USE_POWERSHELL_TOOL` since hooks spawn PowerShell directly. Ignored when `args` is set |
459443| `onFailure` | no | Whether the action that triggered the hook goes ahead when the hook fails: `"continue"` or `"block"`. See [Block the action when a hook fails](#block-the-action-when-a-hook-fails) for `"block"` and for leaving the field unset. Requires Claude Code v2.1.295 or later |
460444
461<a id="exec-form-and-shell-form" />
462
463445##### Exec form and shell form
464446
465447A command hook runs as exec form when `args` is set, and shell form when `args` is omitted. Set `args` whenever the hook references a [path placeholder](#reference-scripts-by-path), since each element is passed as one argument with no quoting. Omit `args` when you need shell features like pipes or `&&`, or when neither concern applies.
from line 450
468450
469451**Shell form** runs when `args` is absent. The `command` string is passed to a shell: `sh -c` on macOS and Linux, Git Bash on Windows, or PowerShell when Git Bash isn't installed. Set the `shell` field to choose explicitly. The shell tokenizes the string, expands variables, and interprets pipes, `&&`, redirects, and globs.
470452
471<Note>
472 On Windows, exec form requires `command` to resolve to a real executable such as a `.exe`. The `.cmd` and `.bat` shims that npm, npx, eslint, and other tools install in `node_modules/.bin` are not executables and can't be spawned without a shell. To run them in exec form, invoke the underlying script with `node` directly, for example `"command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/node_modules/eslint/bin/eslint.js"]`. The `node` plus script-path pattern works on every platform because `node.exe` is a real binary. To run a `.cmd` or `.bat` shim by name, use shell form.
473</Note>
453> [!NOTE]
454>
455> On Windows, exec form requires `command` to resolve to a real executable such as a `.exe`. The `.cmd` and `.bat` shims that npm, npx, eslint, and other tools install in `node_modules/.bin` are not executables and can't be spawned without a shell. To run them in exec form, invoke the underlying script with `node` directly, for example `"command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/node_modules/eslint/bin/eslint.js"]`. The `node` plus script-path pattern works on every platform because `node.exe` is a real binary. To run a `.cmd` or `.bat` shim by name, use shell form.
474456
475457This example runs a Node script bundled with a plugin. Exec form passes the resolved script path as one argument with no quoting:
476458
from line 479
497479
498480A shell-form plugin hook whose `command` references `${user_config.*}` fails with an [error](/docs/en/errors#plugin-command-references-user-config) instead of running. To use an option value from a shell-form hook, read the `$CLAUDE_PLUGIN_OPTION_<KEY>` environment variable, such as `$CLAUDE_PLUGIN_OPTION_WEBHOOK_URL` for a `webhook_url` option, or set `args` to switch the hook to exec form. Before v2.1.207, shell-form plugin hook commands also substituted `${user_config.*}`.
499481
500<Note>
501 In exec form, `command` is the executable name or path only. If `command` is a bare name with no path separator and contains whitespace alongside `args`, Claude Code logs a warning because the spawn will fail: there is no executable named `node script.js`. Move the extra tokens into `args`. Absolute paths with spaces, such as `C:\Program Files\nodejs\node.exe`, are a single valid executable and don't trigger the warning.
502</Note>
482> [!NOTE]
483>
484> In exec form, `command` is the executable name or path only. If `command` is a bare name with no path separator and contains whitespace alongside `args`, Claude Code logs a warning because the spawn will fail: there is no executable named `node script.js`. Move the extra tokens into `args`. Absolute paths with spaces, such as `C:\Program Files\nodejs\node.exe`, are a single valid executable and don't trigger the warning.
503485
504486#### HTTP hook fields
505487
from line 586
604586* `${CLAUDE_PLUGIN_ROOT}`: the plugin's installation directory, for scripts bundled with a [plugin](/docs/en/plugins/overview). See [plugin environment variables](/docs/en/plugins/manifest-reference#environment-variables) for how the path behaves across updates.
605587* `${CLAUDE_PLUGIN_DATA}`: the plugin's [persistent data directory](/docs/en/plugins/components#path-variables-and-persistent-data), for dependencies and state that should survive plugin updates.
606588
607<Note>
608 **Worktrees are different.** If Claude enters a [worktree](/docs/en/worktrees) during the session, Claude Code keeps `${CLAUDE_PROJECT_DIR}` where it was and passes the worktree path to your hooks a different way:
609
610 * **`${CLAUDE_PROJECT_DIR}` stays put**: it still points at the project root where the session started, so a command such as `${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh` still runs the script in the main checkout.
611 * **`cwd` follows Claude**: the `cwd` field in the hook's [input JSON](#common-input-fields) is the worktree root after Claude enters a worktree, and the new directory after Claude runs `cd`. Read it when a hook needs to know which directory Claude is working in.
612
613 For a worktree session that you start from the desktop app, see [What worktrees share with the main checkout](/docs/en/worktrees#what-worktrees-share-with-the-main-checkout) for where `${CLAUDE_PROJECT_DIR}` points.
614</Note>
589> [!NOTE]
590>
591> **Worktrees are different.** If Claude enters a [worktree](/docs/en/worktrees) during the session, Claude Code keeps `${CLAUDE_PROJECT_DIR}` where it was and passes the worktree path to your hooks a different way:
592>
593> * **`${CLAUDE_PROJECT_DIR}` stays put**: it still points at the project root where the session started, so a command such as `${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh` still runs the script in the main checkout.
594> * **`cwd` follows Claude**: the `cwd` field in the hook's [input JSON](#common-input-fields) is the worktree root after Claude enters a worktree, and the new directory after Claude runs `cd`. Read it when a hook needs to know which directory Claude is working in.
595>
596> For a worktree session that you start from the desktop app, see [What worktrees share with the main checkout](/docs/en/worktrees#what-worktrees-share-with-the-main-checkout) for where `${CLAUDE_PROJECT_DIR}` points.
615597
616598Prefer [exec form](#exec-form-and-shell-form) for any hook that references a path placeholder. In shell form, wrap each placeholder in double quotes.
617599
618<Tabs>
619 <Tab title="Project scripts">
620 This example uses `${CLAUDE_PROJECT_DIR}` to run a style checker from the project's `.claude/hooks/` directory after any `Write` or `Edit` tool call:
621
622 ```json theme={null}
623 {
624 "hooks": {
625 "PostToolUse": [
600**Project scripts**
601
602This example uses `${CLAUDE_PROJECT_DIR}` to run a style checker from the project's `.claude/hooks/` directory after any `Write` or `Edit` tool call:
603
604```json theme={null}
605{
606 "hooks": {
607 "PostToolUse": [
608 {
609 "matcher": "Write|Edit",
610 "hooks": [
626611 {
627 "matcher": "Write|Edit",
628 "hooks": [
629 {
630 "type": "command",
631 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
632 "args": []
633 }
634 ]
612 "type": "command",
613 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
614 "args": []
635615 }
636616 ]
637617 }
638 }
639 ```
640 </Tab>
641
642 <Tab title="Plugin scripts">
643 Define plugin hooks in `hooks/hooks.json` with an optional top-level `description` field. When a plugin is enabled, its hooks merge with your user and project hooks.
644
645 This example runs a formatting script bundled with the plugin:
646
647 ```json theme={null}
648 {
649 "description": "Automatic code formatting",
650 "hooks": {
651 "PostToolUse": [
618 ]
619 }
620}
621```
622
623**Plugin scripts**
624
625Define plugin hooks in `hooks/hooks.json` with an optional top-level `description` field. When a plugin is enabled, its hooks merge with your user and project hooks.
626
627This example runs a formatting script bundled with the plugin:
628
629```json theme={null}
630{
631 "description": "Automatic code formatting",
632 "hooks": {
633 "PostToolUse": [
634 {
635 "matcher": "Write|Edit",
636 "hooks": [
652637 {
653 "matcher": "Write|Edit",
654 "hooks": [
655 {
656 "type": "command",
657 "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
658 "args": [],
659 "timeout": 30
660 }
661 ]
638 "type": "command",
639 "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
640 "args": [],
641 "timeout": 30
662642 }
663643 ]
664644 }
665 }
666 ```
667
668 See the [plugin components reference](/docs/en/plugins/components#hooks) for details on creating plugin hooks.
669 </Tab>
670</Tabs>
645 ]
646 }
647}
648```
649
650See the [plugin components reference](/docs/en/plugins/components#hooks) for details on creating plugin hooks.
671651
672652### Hooks in skills and agents
673653
from line 848
868848
869849When you set up a policy hook, watch for this notice on its first run, because a mistyped path in `settings.json` means the hook never runs. To block the action when the hook can't start, set [`onFailure: "block"`](#block-the-action-when-a-hook-fails).
870850
871<Warning>
872 Exit code 1 without valid JSON output is a non-blocking error unless the hook is one [whose failure blocks the action](#block-the-action-when-a-hook-fails), even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use `exit 2`.
873</Warning>
851> [!WARNING]
852>
853> Exit code 1 without valid JSON output is a non-blocking error unless the hook is one [whose failure blocks the action](#block-the-action-when-a-hook-fails), even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use `exit 2`.
874854
875855#### Timeouts
876856
from line 883
903883
904884On most events, when a hook fails or times out, Claude Code still carries out the action. A policy hook with a wrong path or a crashing script then lets everything through. To block the action instead, set `"onFailure": "block"` on a `command` or `http` hook. Requires Claude Code v2.1.295 or later.
905885
906<Note>
907 On Windows without Git Bash, a shell-form `PreToolUse` or `PermissionRequest` command hook that fails with `onFailure` unset can instead [block the tool call](#failed-hooks-on-windows-without-git-bash).
908</Note>
886> [!NOTE]
887>
888> On Windows without Git Bash, a shell-form `PreToolUse` or `PermissionRequest` command hook that fails with `onFailure` unset can instead [block the tool call](#failed-hooks-on-windows-without-git-bash).
909889
910890This `PreToolUse` hook in `.claude/settings.json` runs a project script before each Bash command, and blocks the command if the script fails:
911891
from line 1009
10291009
10301010Exit codes only let you block or stay silent, but JSON output gives you finer-grained control. Instead of exiting with code 2 to block, exit 0 and print a JSON object to stdout. Claude Code reads specific fields from that JSON to control behavior, including [decision control](#decision-control) for blocking, allowing, or escalating to the user.
10311011
1032<Note>
1033 Choose one approach per hook: either use exit codes alone for signaling, or exit 0 and print JSON for structured control. If you mix them, exit 2 keeps its [blocking effect](#exit-code-2-behavior-per-event), and Claude Code still reads the JSON fields, with the one elicitation exception noted under [Exit code 2](#exit-code-2).
1034</Note>
1012> [!NOTE]
1013>
1014> Choose one approach per hook: either use exit codes alone for signaling, or exit 0 and print JSON for structured control. If you mix them, exit 2 keeps its [blocking effect](#exit-code-2-behavior-per-event), and Claude Code still reads the JSON fields, with the one elicitation exception noted under [Exit code 2](#exit-code-2).
10351015
10361016Print nothing but the JSON object to stdout. For a hook that isn't [async](#how-async-hooks-execute), other text there, such as a line your shell profile echoes at startup, keeps Claude Code from reading the object as JSON, and [Hook JSON has no effect](/docs/en/hooks-guide#hook-json-has-no-effect) shows how to find and silence that text.
10371017
from line 1148
11681148
11691149Here are examples of each pattern in action:
11701150
1171<Tabs>
1172 <Tab title="Top-level decision">
1173 The only value for `decision` is `"block"`. To allow the action to proceed, omit `decision` from your JSON, or exit 0 without any JSON at all:
1174
1175 ```json theme={null}
1176 {
1177 "decision": "block",
1178 "reason": "Test suite must pass before proceeding"
1179 }
1180 ```
1181 </Tab>
1182
1183 <Tab title="PreToolUse">
1184 Uses `hookSpecificOutput` for richer control: allow, deny, or escalate to the user. You can also modify tool input before it runs or inject additional context for Claude. See [PreToolUse decision control](#pretooluse-decision-control) for the full set of options.
1185
1186 ```json theme={null}
1187 {
1188 "hookSpecificOutput": {
1189 "hookEventName": "PreToolUse",
1190 "permissionDecision": "deny",
1191 "permissionDecisionReason": "Database writes are not allowed"
1151**Top-level decision**
1152
1153The only value for `decision` is `"block"`. To allow the action to proceed, omit `decision` from your JSON, or exit 0 without any JSON at all:
1154
1155```json theme={null}
1156{
1157 "decision": "block",
1158 "reason": "Test suite must pass before proceeding"
1159}
1160```
1161
1162**PreToolUse**
1163
1164Uses `hookSpecificOutput` for richer control: allow, deny, or escalate to the user. You can also modify tool input before it runs or inject additional context for Claude. See [PreToolUse decision control](#pretooluse-decision-control) for the full set of options.
1165
1166```json theme={null}
1167{
1168 "hookSpecificOutput": {
1169 "hookEventName": "PreToolUse",
1170 "permissionDecision": "deny",
1171 "permissionDecisionReason": "Database writes are not allowed"
1172 }
1173}
1174```
1175
1176**PermissionRequest**
1177
1178Uses `hookSpecificOutput` to allow or deny a permission request on behalf of the user. When allowing, you can also modify the tool's input or apply permission rules so the user isn't prompted again. See [PermissionRequest decision control](#permissionrequest-decision-control) for the full set of options.
1179
1180```json theme={null}
1181{
1182 "hookSpecificOutput": {
1183 "hookEventName": "PermissionRequest",
1184 "decision": {
1185 "behavior": "allow",
1186 "updatedInput": {
1187 "command": "npm run lint"
11921188 }
11931189 }
1194 ```
1195 </Tab>
1196
1197 <Tab title="PermissionRequest">
1198 Uses `hookSpecificOutput` to allow or deny a permission request on behalf of the user. When allowing, you can also modify the tool's input or apply permission rules so the user isn't prompted again. See [PermissionRequest decision control](#permissionrequest-decision-control) for the full set of options.
1199
1200 ```json theme={null}
1201 {
1202 "hookSpecificOutput": {
1203 "hookEventName": "PermissionRequest",
1204 "decision": {
1205 "behavior": "allow",
1206 "updatedInput": {
1207 "command": "npm run lint"
1208 }
1209 }
1210 }
1211 }
1212 ```
1213 </Tab>
1214</Tabs>
1190 }
1191}
1192```
12151193
12161194For extended examples including Bash command validation, prompt filtering, and auto-approval scripts, see [What you can automate](/docs/en/hooks-guide#what-you-can-automate) in the guide and the [Bash command validator reference implementation](https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py).
12171195
from line 1344
13661344
13671345Each Bash command runs the file's contents as shell code before the command itself, so a line there can use anything Bash evaluates, such as the `$PATH` reference in `export PATH="$PATH:./node_modules/.bin"`.
13681346
1369<a id="persisted-variables-in-powershell-commands" />
1370
13711347##### Persisted variables in PowerShell commands
13721348
13731349[PowerShell](/docs/en/tools-reference#powershell-tool) commands receive the variables from `CLAUDE_ENV_FILE` too, in Claude Code v2.1.296 or later, but PowerShell never runs the file. Claude Code reads the assignments out of it and copies them into the PowerShell command's environment instead. It does so only when every line is one of the following, across what every hook in this session wrote and any script you [set `CLAUDE_ENV_FILE` to](/docs/en/env-vars) before launch:
from line 1353
13771353
13781354If any line is of another kind, such as `export PATH="$PATH:./node_modules/.bin"` with its unescaped `$PATH`, a `source` command, or the `$'...'` strings that `direnv export bash` prints, PowerShell commands receive none of the variables, and `claude --debug` logs `Session environment is not all plain assignments`. Bash commands still receive all of them. On Windows, PowerShell commands also don't receive a variable whose value contains `/` or `\`, because Git Bash and Windows write paths differently. A [sandboxed](/docs/en/sandboxing) PowerShell command receives none of the variables.
13791355
1380<Note>
1381 `CLAUDE_ENV_FILE` is available for SessionStart, [Setup](#setup), [CwdChanged](#cwdchanged), and [FileChanged](#filechanged) hooks. Other hook events don't have access to this variable, and neither does a hook that runs in PowerShell, whether through [`"shell": "powershell"`](#command-hook-fields) or by default on Windows without Git Bash.
1382</Note>
1356> [!NOTE]
1357>
1358> `CLAUDE_ENV_FILE` is available for SessionStart, [Setup](#setup), [CwdChanged](#cwdchanged), and [FileChanged](#filechanged) hooks. Other hook events don't have access to this variable, and neither does a hook that runs in PowerShell, whether through [`"shell": "powershell"`](#command-hook-fields) or by default on Windows without Git Bash.
13831359
13841360### Setup
13851361
from line 1618
16421618
16431619This example strips markdown formatting from Claude's responses for a plain-text display. The script reads each batch from stdin, removes bold markers and inline code backticks from `delta`, and returns the result as `displayContent`.
16441620
1645<Tabs>
1646 <Tab title="macOS/Linux">
1647 Register a command hook for the event in your settings file:
1648
1649 ```json theme={null}
1650 {
1651 "hooks": {
1652 "MessageDisplay": [
1621**macOS/Linux**
1622
1623Register a command hook for the event in your settings file:
1624
1625```json theme={null}
1626{
1627 "hooks": {
1628 "MessageDisplay": [
1629 {
1630 "hooks": [
16531631 {
1654 "hooks": [
1655 {
1656 "type": "command",
1657 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.sh",
1658 "args": []
1659 }
1632 "type": "command",
1633 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.sh",
1634 "args": []
1635 }
1636 ]
1637 }
1638 ]
1639 }
1640}
1641```
1642
1643Save this script to `.claude/hooks/plain-display.sh` in your project and make it executable with `chmod +x`:
1644
1645```bash theme={null}
1646#!/bin/bash
1647jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'
1648```
1649
1650**Windows (PowerShell)**
1651
1652Register a command hook that runs the script through PowerShell:
1653
1654```json theme={null}
1655{
1656 "hooks": {
1657 "MessageDisplay": [
1658 {
1659 "hooks": [
1660 {
1661 "type": "command",
1662 "command": "powershell.exe",
1663 "args": [
1664 "-NoProfile",
1665 "-ExecutionPolicy",
1666 "Bypass",
1667 "-File",
1668 "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.ps1"
16601669 ]
16611670 }
16621671 ]
16631672 }
1664 }
1665 ```
1666
1667 Save this script to `.claude/hooks/plain-display.sh` in your project and make it executable with `chmod +x`:
1668
1669 ```bash theme={null}
1670 #!/bin/bash
1671 jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'
1672 ```
1673 </Tab>
1674
1675 <Tab title="Windows (PowerShell)">
1676 Register a command hook that runs the script through PowerShell:
1677
1678 ```json theme={null}
1679 {
1680 "hooks": {
1681 "MessageDisplay": [
1682 {
1683 "hooks": [
1684 {
1685 "type": "command",
1686 "command": "powershell.exe",
1687 "args": [
1688 "-NoProfile",
1689 "-ExecutionPolicy",
1690 "Bypass",
1691 "-File",
1692 "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.ps1"
1693 ]
1694 }
1695 ]
1696 }
1697 ]
1698 }
1699 }
1700 ```
1701
1702 The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
1703
1704 Save this script to `.claude/hooks/plain-display.ps1` in your project:
1705
1706 ```powershell theme={null}
1707 $batch = [Console]::In.ReadToEnd() | ConvertFrom-Json
1708 $text = $batch.delta -replace '\*\*', '' -replace '`', ''
1709 @{
1710 hookSpecificOutput = @{
1711 hookEventName = "MessageDisplay"
1712 displayContent = $text
1713 }
1714 } | ConvertTo-Json
1715 ```
1716 </Tab>
1717</Tabs>
1673 ]
1674 }
1675}
1676```
1677
1678The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.
1679
1680Save this script to `.claude/hooks/plain-display.ps1` in your project:
1681
1682```powershell theme={null}
1683$batch = [Console]::In.ReadToEnd() | ConvertFrom-Json
1684$text = $batch.delta -replace '\*\*', '' -replace '`', ''
1685@{
1686 hookSpecificOutput = @{
1687 hookEventName = "MessageDisplay"
1688 displayContent = $text
1689 }
1690} | ConvertTo-Json
1691```
17181692
17191693Batches with no markdown pass through unchanged. If the script fails, for example because `jq` is missing, Claude Code displays the original text and notes the failure only in [debug output](#debug-hooks), not in the session.
17201694
from line 1698
17241698
17251699To run a hook when a specific file changes on disk, whatever wrote it, use [FileChanged](#filechanged) instead of matching file-editing tools by name. Unlike PreToolUse, Claude Code runs FileChanged hooks after the change, and they have no decision control, so they can't block the write.
17261700
1727<Warning>
1728 PreToolUse runs only when Claude calls a tool. By default, files you [reference with `@` in your prompt](/docs/en/common-workflows#reference-files-and-directories) are added without any tool call. Claude Code inserts their contents while building the prompt, so no PreToolUse hook fires for them, including hooks matching `Read`. PreToolUse also doesn't fire for [`EndConversation`](/docs/en/tools-reference#endconversation-tool-behavior).
1729
1730 You can block specific paths from `@` references, or turn the insertion off:
1731
1732 * **Block specific paths**: add a [`Read` deny rule](/docs/en/permissions#read-and-edit) for them
1733 * **Turn the insertion off**: set [`CLAUDE_CODE_DISABLE_FILE_MENTIONS=1`](/docs/en/env-vars#variables) in your shell, user settings, or managed settings. Claude then gets the mentioned paths with a note to read them using its file tools. If it does, your matching PreToolUse hooks run. The variable requires Claude Code v2.1.299 or later, and Claude Code ignores it in project and local settings.
1734</Warning>
1701> [!WARNING]
1702>
1703> PreToolUse runs only when Claude calls a tool. By default, files you [reference with `@` in your prompt](/docs/en/common-workflows#reference-files-and-directories) are added without any tool call. Claude Code inserts their contents while building the prompt, so no PreToolUse hook fires for them, including hooks matching `Read`. PreToolUse also doesn't fire for [`EndConversation`](/docs/en/tools-reference#endconversation-tool-behavior).
1704>
1705> You can block specific paths from `@` references, or turn the insertion off:
1706>
1707> * **Block specific paths**: add a [`Read` deny rule](/docs/en/permissions#read-and-edit) for them
1708> * **Turn the insertion off**: set [`CLAUDE_CODE_DISABLE_FILE_MENTIONS=1`](/docs/en/env-vars#variables) in your shell, user settings, or managed settings. Claude then gets the mentioned paths with a note to read them using its file tools. If it does, your matching PreToolUse hooks run. The variable requires Claude Code v2.1.299 or later, and Claude Code ignores it in project and local settings.
17351709
17361710Use [PreToolUse decision control](#pretooluse-decision-control) to allow, deny, ask, or defer the tool call.
17371711
from line 1740
17661740
17671741The `tool_input` fields depend on the tool:
17681742
1769<a id="bash" />
1770
17711743##### Bash
17721744
17731745Executes shell commands.
from line 1755
17831755
17841756Your [PostToolUse hook](#posttooluse) then receives the changed files in `tool_response.bashEditDiff`. The list covers what changed under the repository while the command ran. Files that Git ignores and files in submodules aren't listed. Requires Claude Code v2.1.269 or later.
17851757
1786<Note>
1787 The list is best effort and in public beta. Claude Code can miss a change, include a file that another process changed at the same time, or stop at its size limits. The field shape may change. Use the list to find what to review, not to enforce a policy.
1788</Note>
1758> [!NOTE]
1759>
1760> The list is best effort and in public beta. Claude Code can miss a change, include a file that another process changed at the same time, or stop at its size limits. The field shape may change. Use the list to find what to review, not to enforce a policy.
17891761
17901762`changedFiles` and `files` list what the command changed; the remaining fields say how complete and how reliable that list is.
17911763
from line 1769
17971769| `unavailable` | boolean | `true` | Set when the diff is incomplete or couldn't be taken |
17981770| `skipped` | boolean | `true` | Set for a Git command that moves the working tree, such as `git checkout` or `git stash`, so Claude Code takes no diff |
17991771| `shared` | boolean | `true` | Set when another Bash tool call, such as a subagent's, ran in the same repository at the same time, so some listed changes may be that command's |
1800
1801<a id="powershell" />
18021772
18031773##### PowerShell
18041774
from line 1892
19221892
19231893On a `completed` response, `resolvedModel` names the model the subagent started on, which can differ from the `model` value in `tool_input`, such as when `availableModels` or another override applies. On an `async_launched` response, `resolvedModel` names the model in use when the agent moved to the background, so a swap that happened before backgrounding is reflected there.
19241894
1925<a id="askuserquestion" />
1926
19271895##### AskUserQuestion
19281896
19291897Asks the user one to four multiple-choice questions.
from line 1946
19781946}
19791947```
19801948
1981<Note>
1982 PreToolUse previously used top-level `decision` and `reason` fields, but these are deprecated for this event. Use `hookSpecificOutput.permissionDecision` and `hookSpecificOutput.permissionDecisionReason` instead. The deprecated values `"approve"` and `"block"` map to `"allow"` and `"deny"` respectively. Other events like PostToolUse and Stop continue to use top-level `decision` and `reason` as their current format.
1983</Note>
1984
1985<h4 id="allow-with-updatedinput">
1986 Tools that require user interaction
1987</h4>
1949> [!NOTE]
1950>
1951> PreToolUse previously used top-level `decision` and `reason` fields, but these are deprecated for this event. Use `hookSpecificOutput.permissionDecision` and `hookSpecificOutput.permissionDecisionReason` instead. The deprecated values `"approve"` and `"block"` map to `"allow"` and `"deny"` respectively. Other events like PostToolUse and Stop continue to use top-level `decision` and `reason` as their current format.
1952
1953#### Tools that require user interaction
19881954
19891955`AskUserQuestion` and `ExitPlanMode` require user interaction. In [non-interactive mode](/docs/en/headless) with the `-p` flag, Claude Code offers them only when the run has a [permission host](/docs/en/headless#turn-off-permission-prompts-in-unattended-runs) to receive the prompt, such as an Agent SDK `canUseTool` callback.
19901956
from line 2020
20542020
20552021If the deferred tool is no longer available when you resume, the process exits with `stop_reason: "tool_deferred_unavailable"` and `is_error: true` before the hook fires. This happens when an MCP server that provided the tool is not connected for the resumed session. The `deferred_tool_use` payload is still included so you can identify which tool went missing.
20562022
2057<Note>
2058 To resume a deferred session in plan mode, pass [`--permission-prompt-tool`](/docs/en/cli-reference#cli-flags) along with `--resume` so that Claude Code can present the plan for approval. For the other conditions, see [Resume in plan mode with `-p`](/docs/en/sessions#resume-in-plan-mode-with-p). Requires Claude Code v2.1.246 or later.
2059
2060 When you resume with `-p`, Claude Code doesn't restore any other stored permission mode. It starts the run in the permission mode a new `claude -p` run would start in, so pass `--permission-mode` or `--dangerously-skip-permissions` again if the deferred session used one. When you resume with `claude --resume <session-id>` without `-p`, Claude Code restores the stored permission mode, with the exceptions listed in [permission mode on resume](/docs/en/sessions#permission-mode-on-resume).
2061</Note>
2023> [!NOTE]
2024>
2025> To resume a deferred session in plan mode, pass [`--permission-prompt-tool`](/docs/en/cli-reference#cli-flags) along with `--resume` so that Claude Code can present the plan for approval. For the other conditions, see [Resume in plan mode with `-p`](/docs/en/sessions#resume-in-plan-mode-with-p). Requires Claude Code v2.1.246 or later.
2026>
2027> When you resume with `-p`, Claude Code doesn't restore any other stored permission mode. It starts the run in the permission mode a new `claude -p` run would start in, so pass `--permission-mode` or `--dangerously-skip-permissions` again if the deferred session used one. When you resume with `claude --resume <session-id>` without `-p`, Claude Code restores the stored permission mode, with the exceptions listed in [permission mode on resume](/docs/en/sessions#permission-mode-on-resume).
20622028
20632029### PermissionRequest
20642030
from line 2109
21432109| `addDirectories` | `directories`, `destination` | Adds working directories. `directories` is an array of path strings |
21442110| `removeDirectories` | `directories`, `destination` | Removes working directories |
21452111
2146<Note>
2147 `setMode` with `bypassPermissions` only takes effect if you launched the session with bypass mode already available: `--dangerously-skip-permissions`, `--permission-mode bypassPermissions`, `--allow-dangerously-skip-permissions`, or `permissions.defaultMode: "bypassPermissions"` in [user, `--settings`, or managed settings](/docs/en/settings-reference#permissions-defaultmode). Otherwise the update is a no-op. The update is also a no-op when [`permissions.disableBypassPermissionsMode`](/docs/en/permissions#managed-settings) disables the mode, or when the session starts in [restricted mode](/docs/en/cli-reference#cli-flags).
2148
2149 `bypassPermissions` is never persisted as `defaultMode` regardless of `destination`.
2150</Note>
2112> [!NOTE]
2113>
2114> `setMode` with `bypassPermissions` only takes effect if you launched the session with bypass mode already available: `--dangerously-skip-permissions`, `--permission-mode bypassPermissions`, `--allow-dangerously-skip-permissions`, or `permissions.defaultMode: "bypassPermissions"` in [user, `--settings`, or managed settings](/docs/en/settings-reference#permissions-defaultmode). Otherwise the update is a no-op. The update is also a no-op when [`permissions.disableBypassPermissionsMode`](/docs/en/permissions#managed-settings) disables the mode, or when the session starts in [restricted mode](/docs/en/cli-reference#cli-flags).
2115>
2116> `bypassPermissions` is never persisted as `defaultMode` regardless of `destination`.
21512117
21522118The `destination` field on every entry determines whether the change stays in memory or persists to a settings file.
21532119
from line 2196
22302196}
22312197```
22322198
2233<Warning>
2234 `updatedToolOutput` only changes what Claude sees. The tool has already run by the time the hook fires, so any files written, commands executed, or network requests sent have already taken effect. Telemetry such as OpenTelemetry tool spans and analytics events also captures the original output before the hook runs. To prevent or modify a tool call before it runs, use a [PreToolUse](#pretooluse) hook instead.
2235
2236 The replacement value must match the tool's output shape. Built-in tools return structured objects rather than plain strings. For example, `Bash` returns an object with `stdout`, `stderr`, `interrupted`, and `isImage` fields. For built-in tools, a value that doesn't match the tool's output schema is ignored and the original output is used. MCP tool output is passed through without schema validation. Stripping error details that Claude needs can cause it to proceed on a false assumption.
2237</Warning>
2199> [!WARNING]
2200>
2201> `updatedToolOutput` only changes what Claude sees. The tool has already run by the time the hook fires, so any files written, commands executed, or network requests sent have already taken effect. Telemetry such as OpenTelemetry tool spans and analytics events also captures the original output before the hook runs. To prevent or modify a tool call before it runs, use a [PreToolUse](#pretooluse) hook instead.
2202>
2203> The replacement value must match the tool's output shape. Built-in tools return structured objects rather than plain strings. For example, `Bash` returns an object with `stdout`, `stderr`, `interrupted`, and `isImage` fields. For built-in tools, a value that doesn't match the tool's output schema is ignored and the original output is used. MCP tool output is passed through without schema validation. Stripping error details that Claude needs can cause it to proceed on a false assumption.
22382204
22392205#### Annotate a result for the auto mode classifier
22402206
from line 2229
22632229* **Calls that the classifier doesn't record**: the classifier's transcript omits read-only lookups such as file reads and searches. Claude Code discards a note attached to one of those calls
22642230* **Interaction with rewrites**: when the note describes output you're replacing with `updatedToolOutput`, return both fields in the same hook response. Claude Code drops the note if that rewrite is rejected or another hook's rewrite replaces it. Claude Code delivers a note you return without a rewrite even when another hook rewrites the output
22652231
2266<Warning>
2267 The classifier reads content you place in `classifierContext` as information from the application hosting the session, so don't copy untrusted tool output or third-party text into it. Keep the note to a short assertion about this one call, such as a fact about its origin or a user statement about it; don't use the field to deliver unrelated messages or a stream of events.
2268</Warning>
2232> [!WARNING]
2233>
2234> The classifier reads content you place in `classifierContext` as information from the application hosting the session, so don't copy untrusted tool output or third-party text into it. Keep the note to a short assertion about this one call, such as a fact about its origin or a user statement about it; don't use the field to deliver unrelated messages or a stream of events.
22692235
22702236### PostToolUseFailure
22712237
from line 2239
22732239
22742240Matches on tool name, same values as PreToolUse.
22752241
2276<Note>
2277 This event doesn't fire for tool calls rejected before execution: an unknown tool name, input that fails schema or tool-specific validation, or a permission denial. Validation rejections are returned as `tool_use_error` results and happen before hooks run, so they fire neither `PreToolUse` nor `PostToolUseFailure`. Permission denials fire `PreToolUse` but not this event; see [PermissionDenied](#permissiondenied).
2278</Note>
2242> [!NOTE]
2243>
2244> This event doesn't fire for tool calls rejected before execution: an unknown tool name, input that fails schema or tool-specific validation, or a permission denial. Validation rejections are returned as `tool_use_error` results and happen before hooks run, so they fire neither `PreToolUse` nor `PostToolUseFailure`. Permission denials fire `PreToolUse` but not this event; see [PermissionDenied](#permissiondenied).
22792245
22802246#### PostToolUseFailure input
22812247
from line 2329
23632329
23642330`tool_response` contains the same content the model receives in the corresponding `tool_result` block. The value is a serialized string or content-block array, exactly as the tool emitted it. For `Read`, that means line-number-prefixed text rather than raw file contents. Responses can be large, so parse only the fields you need.
23652331
2366<Note>
2367 The `tool_response` shape differs from `PostToolUse`'s. `PostToolUse` passes the tool's structured `Output` object, such as `{filePath: "...", type: "create"}` for `Write`; `PostToolBatch` passes the serialized `tool_result` content the model sees.
2368</Note>
2332> [!NOTE]
2333>
2334> The `tool_response` shape differs from `PostToolUse`'s. `PostToolUse` passes the tool's structured `Output` object, such as `{filePath: "...", type: "create"}` for `Write`; `PostToolBatch` passes the serialized `tool_result` content the model sees.
23692335
23702336#### PostToolBatch decision control
23712337
from line 2427
24612427
24622428`agent_needs_input` for a teammate's terminal setup question requires Claude Code v2.1.248 or later.
24632429
2464<Note>
2465 The `permission_prompt`, `idle_prompt`, `elicitation_dialog`, and `elicitation_url_dialog` types share their timing with desktop notifications, so in terminal sessions you only see them when you appear to be away from the terminal:
2466
2467 * Expect `permission_prompt` once you haven't typed for about six seconds. The timer starts when the permission prompt appears, and each keystroke defers it. To run a hook immediately when Claude asks for permission to use a tool, use [PermissionRequest](#permissionrequest) instead.
2468 * Expect `idle_prompt` about 60 seconds after Claude finishes responding, and only if you haven't typed since and no background agent, such as a background [subagent](/docs/en/sub-agents), is still running. Claude Code doesn't send `idle_prompt` while it waits for a claude.ai usage limit to reset. When the wait ends on its own, one of the `quota_auto_resume_*` types fires instead.
2469 * Expect `elicitation_dialog` for an elicitation form, or `elicitation_url_dialog` for a browser URL request, once you haven't typed for about six seconds. Both share the same six-second gate as `permission_prompt`: the timer starts when the dialog appears, and each keystroke defers it.
2470
2471 A permission request or elicitation that arrives while another dialog is on screen keeps the same six-second gate, timed from when the request arrives. Its notification can reach you while the request still waits behind the open dialog.
2472</Note>
2430> [!NOTE]
2431>
2432> The `permission_prompt`, `idle_prompt`, `elicitation_dialog`, and `elicitation_url_dialog` types share their timing with desktop notifications, so in terminal sessions you only see them when you appear to be away from the terminal:
2433>
2434> * Expect `permission_prompt` once you haven't typed for about six seconds. The timer starts when the permission prompt appears, and each keystroke defers it. To run a hook immediately when Claude asks for permission to use a tool, use [PermissionRequest](#permissionrequest) instead.
2435> * Expect `idle_prompt` about 60 seconds after Claude finishes responding, and only if you haven't typed since and no background agent, such as a background [subagent](/docs/en/sub-agents), is still running. Claude Code doesn't send `idle_prompt` while it waits for a claude.ai usage limit to reset. When the wait ends on its own, one of the `quota_auto_resume_*` types fires instead.
2436> * Expect `elicitation_dialog` for an elicitation form, or `elicitation_url_dialog` for a browser URL request, once you haven't typed for about six seconds. Both share the same six-second gate as `permission_prompt`: the timer starts when the dialog appears, and each keystroke defers it.
2437>
2438> A permission request or elicitation that arrives while another dialog is on screen keeps the same six-second gate, timed from when the request arrives. Its notification can reach you while the request still waits behind the open dialog.
24732439
24742440Claude Code times `permission_prompt` differently in sessions where it sends permission requests to the Agent SDK's [`canUseTool` callback](/docs/en/agent-sdk/user-input), which is how Claude Desktop and the VS Code extension host Claude Code:
24752441
from line 2683
27172683the stoppage occurred due to a user interrupt. API errors fire
27182684[StopFailure](#stopfailure) instead.
27192685
2720<Tip>
2721 The [`/goal`](/docs/en/goal) command is a built-in shortcut for a session-scoped prompt-based Stop hook. Use it when you want Claude to keep working toward a condition without writing hook configuration.
2722</Tip>
2686> [!TIP]
2687>
2688> The [`/goal`](/docs/en/goal) command is a built-in shortcut for a session-scoped prompt-based Stop hook. Use it when you want Claude to keep working toward a condition without writing hook configuration.
27232689
27242690#### Stop input
27252691
from line 3306
33403306
33413307Write the matcher as an exact name, a `|`-separated list such as `claude-opus-4-6|claude-opus-5`, or a regular expression such as `.*opus.*`. This example uses an exact-name matcher and also checks `to_model` from the hook input, so it refuses a switch to Opus 4.6 by exiting with code 2 and lets any other target through:
33423308
3343<Tabs>
3344 <Tab title="macOS/Linux">
3345 The command checks `to_model` with `jq`:
3346
3347 ```json theme={null}
3348 {
3349 "hooks": {
3350 "PreModelSwitch": [
3309**macOS/Linux**
3310
3311The command checks `to_model` with `jq`:
3312
3313```json theme={null}
3314{
3315 "hooks": {
3316 "PreModelSwitch": [
3317 {
3318 "matcher": "claude-opus-4-6",
3319 "hooks": [
33513320 {
3352 "matcher": "claude-opus-4-6",
3353 "hooks": [
3354 {
3355 "type": "command",
3356 "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
3357 }
3321 "type": "command",
3322 "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
3323 }
3324 ]
3325 }
3326 ]
3327 }
3328}
3329```
3330
3331**Windows (PowerShell)**
3332
3333Register a command hook that runs a script through PowerShell:
3334
3335```json theme={null}
3336{
3337 "hooks": {
3338 "PreModelSwitch": [
3339 {
3340 "matcher": "claude-opus-4-6",
3341 "hooks": [
3342 {
3343 "type": "command",
3344 "command": "powershell.exe",
3345 "args": [
3346 "-NoProfile",
3347 "-ExecutionPolicy",
3348 "Bypass",
3349 "-File",
3350 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-opus-46.ps1"
33583351 ]
33593352 }
33603353 ]
33613354 }
3362 }
3363 ```
3364 </Tab>
3365
3366 <Tab title="Windows (PowerShell)">
3367 Register a command hook that runs a script through PowerShell:
3368
3369 ```json theme={null}
3370 {
3371 "hooks": {
3372 "PreModelSwitch": [
3373 {
3374 "matcher": "claude-opus-4-6",
3375 "hooks": [
3376 {
3377 "type": "command",
3378 "command": "powershell.exe",
3379 "args": [
3380 "-NoProfile",
3381 "-ExecutionPolicy",
3382 "Bypass",
3383 "-File",
3384 "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-opus-46.ps1"
3385 ]
3386 }
3387 ]
3388 }
3389 ]
3390 }
3391 }
3392 ```
3393
3394 Save this script to `.claude/hooks/block-opus-46.ps1` in your project:
3395
3396 ```powershell theme={null}
3397 $hookInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
3398 if ($hookInput.to_model -match 'opus-4-6') {
3399 [Console]::Error.WriteLine('Opus 4.6 is retired for this project. Use a newer model.')
3400 exit 2
3401 }
3402 exit 0
3403 ```
3404 </Tab>
3405</Tabs>
3355 ]
3356 }
3357}
3358```
3359
3360Save this script to `.claude/hooks/block-opus-46.ps1` in your project:
3361
3362```powershell theme={null}
3363$hookInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
3364if ($hookInput.to_model -match 'opus-4-6') {
3365 [Console]::Error.WriteLine('Opus 4.6 is retired for this project. Use a newer model.')
3366 exit 2
3367}
3368exit 0
3369```
34063370
34073371To confirm the hook works, run `/model claude-opus-4-6` from a session running a different model. Claude Code keeps the current model and reports that a PreModelSwitch hook blocked the switch, with your message as the reason.
34083372
from line 3654
36903654
36913655This example answers one recurring question for the user. An MCP server named `issue-tracker` asks for a project key in a form, and the hook fills in `DOCS`. The script accepts when `project_key` is the form's single field. For any other request it prints nothing, so the dialog appears.
36923656
3693<Tabs>
3694 <Tab title="macOS/Linux">
3695 Register a command hook for the event in your settings file, with the server name as the matcher:
3696
3697 ```json theme={null}
3698 {
3699 "hooks": {
3700 "Elicitation": [
3657**macOS/Linux**
3658
3659Register a command hook for the event in your settings file, with the server name as the matcher:
3660
3661```json theme={null}
3662{
3663 "hooks": {
3664 "Elicitation": [
3665 {
3666 "matcher": "issue-tracker",
3667 "hooks": [
37013668 {
3702 "matcher": "issue-tracker",
3703 "hooks": [
3704 {
3705 "type": "command",
3706 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.sh",
3707 "args": []
3708 }
3669 "type": "command",
3670 "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.sh",
3671 "args": []
3672 }
3673 ]
3674 }
3675 ]
3676 }
3677}
3678```
3679
3680Save this script to `.claude/hooks/answer-project-key.sh` in your project and make it executable with `chmod +x`:
3681
3682```bash theme={null}
3683#!/bin/bash
3684input=$(cat)
3685fields=$(jq -c '.requested_schema.properties // {} | keys' <<<"$input")
3686
3687if [ "$fields" = '["project_key"]' ]; then
3688 jq -n '{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept", content: {project_key: "DOCS"}}}'
3689fi
3690```
3691
3692**Windows (PowerShell)**
3693
3694Register a command hook that runs the script through PowerShell, with the server name as the matcher:
3695
3696```json theme={null}
3697{
3698 "hooks": {
3699 "Elicitation": [
3700 {
3701 "matcher": "issue-tracker",
3702 "hooks": [
3703 {
3704 "type": "command",
3705 "command": "powershell.exe",
3706 "args": [
3707 "-NoProfile",
3708 "-ExecutionPolicy",
3709 "Bypass",
3710 "-File",
3711 "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.ps1"
37093712 ]
37103713 }
37113714 ]
37123715 }
3716 ]
3717 }
3718}
3719```
3720
3721Save this script to `.claude/hooks/answer-project-key.ps1` in your project:
3722
3723```powershell theme={null}
3724$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
3725$fields = @($request.requested_schema.properties.PSObject.Properties.Name)
3726
3727if ($fields.Count -eq 1 -and $fields[0] -eq 'project_key') {
3728 @{
3729 hookSpecificOutput = @{
3730 hookEventName = "Elicitation"
3731 action = "accept"
3732 content = @{ project_key = "DOCS" }
37133733 }
3714 ```
3715
3716 Save this script to `.claude/hooks/answer-project-key.sh` in your project and make it executable with `chmod +x`:
3717
3718 ```bash theme={null}
3719 #!/bin/bash
3720 input=$(cat)
3721 fields=$(jq -c '.requested_schema.properties // {} | keys' <<<"$input")
3722
3723 if [ "$fields" = '["project_key"]' ]; then
3724 jq -n '{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept", content: {project_key: "DOCS"}}}'
3725 fi
3726 ```
3727 </Tab>
3728
3729 <Tab title="Windows (PowerShell)">
3730 Register a command hook that runs the script through PowerShell, with the server name as the matcher:
3731
3732 ```json theme={null}
3733 {
3734 "hooks": {
3735 "Elicitation": [
3736 {
3737 "matcher": "issue-tracker",
3738 "hooks": [
3739 {
3740 "type": "command",
3741 "command": "powershell.exe",
3742 "args": [
3743 "-NoProfile",
3744 "-ExecutionPolicy",
3745 "Bypass",
3746 "-File",
3747 "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.ps1"
3748 ]
3749 }
3750 ]
3751 }
3752 ]
3753 }
3754 }
3755 ```
3756
3757 Save this script to `.claude/hooks/answer-project-key.ps1` in your project:
3758
3759 ```powershell theme={null}
3760 $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
3761 $fields = @($request.requested_schema.properties.PSObject.Properties.Name)
3762
3763 if ($fields.Count -eq 1 -and $fields[0] -eq 'project_key') {
3764 @{
3765 hookSpecificOutput = @{
3766 hookEventName = "Elicitation"
3767 action = "accept"
3768 content = @{ project_key = "DOCS" }
3769 }
3770 } | ConvertTo-Json -Depth 3
3771 }
3772 ```
3773 </Tab>
3774</Tabs>
3734 } | ConvertTo-Json -Depth 3
3735}
3736```
37753737
37763738To confirm the hook works, start Claude Code with `claude --debug` and give Claude a task that makes the server ask for the project key. No dialog appears, and the [debug log](#debug-hooks) has a line that ends with `Elicitation resolved by hook: {"action":"accept","content":{"project_key":"DOCS"}}`.
37773739
from line 3956
39943956
39953957## Agent-based hooks
39963958
3997<Warning>
3998 Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](#command-hook-fields).
3999</Warning>
3959> [!WARNING]
3960>
3961> Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](#command-hook-fields).
40003962
40013963Agent-based hooks (`type: "agent"`) are like prompt-based hooks but with multi-turn tool access. Instead of a single LLM call, an agent hook spawns a subagent that can read files, search code, and inspect the codebase to verify conditions. Agent hooks support the same events as [prompt-based hooks](#prompt-based-hooks), except `PermissionRequest`.
40023964
from line 4112
41504112
41514113### Disclaimer
41524114
4153<Warning>
4154 Command hooks execute shell commands with your full user permissions. They can modify, delete, or access any files your user account can access. Review and test all hook commands before adding them to your configuration.
4155</Warning>
4115> [!WARNING]
4116>
4117> Command hooks execute shell commands with your full user permissions. They can modify, delete, or access any files your user account can access. Review and test all hook commands before adding them to your configuration.
41564118
41574119### Workspace trust
41584120
41594121