Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Hooks reference changedhooks

Nearest release: v2.1.304, published 6 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 20 Oct 2026 02:25 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.

Upstream edited
Recorded here
Lines+511added
Lines−549removed
From line 2 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits112to this page, all time

#### How `if` patterns match Bash commands #### Tools that require user interaction

The whole hunk

from line 2, old and new numbered
/
lines

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![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](https://mintcdn.com/claude-code/x7pO8l4XcvAXCoVc/images/hooks-lifecycle.svg?fit=max\&auto=format\&n=x7pO8l4XcvAXCoVc\&q=85\&s=81b9256c1bbe8832553485f5d9e9c746)
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![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.](https://mintcdn.com/claude-code/ikqp3_70mqIahteV/images/hook-resolution.svg?fit=max\&auto=format\&n=ikqp3_70mqIahteV\&q=85\&s=be0bf3053550c26de5f54cd64674c197)
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 
Feedback