claude_hooks 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +21 -15
  4. data/docs/API/COMMON.md +4 -3
  5. data/docs/API/DIRECTORY_ADDED.md +34 -0
  6. data/docs/API/NOTIFICATION.md +5 -4
  7. data/docs/API/PERMISSION_DENIED.md +4 -0
  8. data/docs/API/PERMISSION_REQUEST.md +3 -3
  9. data/docs/API/POST_MODEL_SWITCH.md +53 -0
  10. data/docs/API/POST_TOOL_USE.md +7 -0
  11. data/docs/API/POST_TOOL_USE_FAILURE.md +1 -0
  12. data/docs/API/PRE_MODEL_SWITCH.md +67 -0
  13. data/docs/API/PRE_TOOL_USE.md +1 -0
  14. data/docs/API/SESSION_START.md +7 -1
  15. data/docs/API/SETUP.md +3 -3
  16. data/docs/API/STOP.md +6 -0
  17. data/docs/API/SUBAGENT_STOP.md +10 -0
  18. data/docs/API/TASK_CREATED.md +9 -7
  19. data/docs/API/USER_PROMPT_SUBMIT.md +14 -0
  20. data/docs/API/WORKTREE_REMOVE.md +9 -4
  21. data/docs/external/claude-hooks-reference.md +1188 -345
  22. data/docs/mitts/prd.md +8 -0
  23. data/docs/mitts/setup.md +5 -3
  24. data/lib/claude_hooks/base.rb +12 -0
  25. data/lib/claude_hooks/directory_added.rb +24 -0
  26. data/lib/claude_hooks/output/base.rb +6 -0
  27. data/lib/claude_hooks/output/directory_added.rb +21 -0
  28. data/lib/claude_hooks/output/notification.rb +10 -1
  29. data/lib/claude_hooks/output/post_model_switch.rb +50 -0
  30. data/lib/claude_hooks/output/post_tool_batch.rb +10 -0
  31. data/lib/claude_hooks/output/post_tool_use.rb +23 -5
  32. data/lib/claude_hooks/output/pre_model_switch.rb +121 -0
  33. data/lib/claude_hooks/output/pre_tool_use.rb +8 -0
  34. data/lib/claude_hooks/output/task_created.rb +34 -1
  35. data/lib/claude_hooks/output/user_prompt_expansion.rb +10 -0
  36. data/lib/claude_hooks/output/user_prompt_submit.rb +19 -6
  37. data/lib/claude_hooks/output/worktree_remove.rb +4 -2
  38. data/lib/claude_hooks/permission_denied.rb +7 -0
  39. data/lib/claude_hooks/permission_request.rb +10 -1
  40. data/lib/claude_hooks/post_model_switch.rb +70 -0
  41. data/lib/claude_hooks/post_tool_use.rb +14 -0
  42. data/lib/claude_hooks/post_tool_use_failure.rb +7 -0
  43. data/lib/claude_hooks/pre_model_switch.rb +90 -0
  44. data/lib/claude_hooks/pre_tool_use.rb +7 -0
  45. data/lib/claude_hooks/session_start.rb +18 -0
  46. data/lib/claude_hooks/setup.rb +3 -0
  47. data/lib/claude_hooks/task_created.rb +17 -0
  48. data/lib/claude_hooks/user_prompt_submit.rb +26 -5
  49. data/lib/claude_hooks/version.rb +1 -1
  50. data/lib/claude_hooks.rb +6 -0
  51. metadata +11 -2
@@ -8,17 +8,17 @@
8
8
 
9
9
  For a quickstart guide with examples, see [Automate actions with hooks](https://code.claude.com/docs/en/hooks-guide).
10
10
 
11
- Hooks are user-defined shell commands, HTTP endpoints, or LLM prompts that execute automatically at specific points in Claude Code’s lifecycle. 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. If you’re setting up hooks for the first time, start with the [guide](https://code.claude.com/docs/en/hooks-guide) instead.
11
+ Hooks 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](https://code.claude.com/docs/en/desktop-quickstart), and [cloud sessions](https://code.claude.com/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.A plugin can also register hooks as JavaScript functions that Claude Code calls in its own process, which can draw in the interface as well as act on events. A plugin that does is a [mod](https://code.claude.com/docs/en/plugins/mods/overview), and those function hooks are covered in [React to events](https://code.claude.com/docs/en/plugins/mods/events) rather than here. The hooks on this page keep working alongside mods.
12
12
 
13
13
  ## [​](https://code.claude.com/docs/en/hooks\#hook-lifecycle) Hook lifecycle
14
14
 
15
- Hooks fire at specific points during a Claude Code session. When an event fires and a matcher matches, Claude Code passes JSON context about the event to your hook handler. For command hooks, input arrives on stdin. For HTTP hooks, it arrives as the POST request body. Your handler can then inspect the input, take action, and optionally return a decision.Events fall into three cadences:
15
+ Claude Code runs hooks at specific points during a session. When an event fires and a matcher matches, Claude Code passes JSON context about the event to your hook handler. For command hooks, input arrives on stdin. For HTTP hooks, it arrives as the POST request body. Your handler can then inspect the input, take action, and optionally return a decision.Events fall into three cadences:
16
16
 
17
- - once per session: `SessionStart` and `SessionEnd`
18
- - once per turn: `UserPromptSubmit`, `Stop`, and `StopFailure`
19
- - on every tool call inside the agentic loop: `PreToolUse` and `PostToolUse`
17
+ - per session: `SessionStart` and `SessionEnd`
18
+ - per turn: `UserPromptSubmit`, `Stop`, and `StopFailure`
19
+ - on every tool call inside the agentic loop: `PreToolUse` and `PostToolUse`, except [`EndConversation`](https://code.claude.com/docs/en/tools-reference#endconversation-tool-behavior) calls, which skip both
20
20
 
21
- ![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, and FileChanged as standalone async events, and MessageDisplay as a display-only event that runs while assistant message text streams](https://mintcdn.com/claude-code/uLsR38F1U_5zPppm/images/hooks-lifecycle.svg?fit=max&auto=format&n=uLsR38F1U_5zPppm&q=85&s=fbdbd78ad9f474da7d344879341341f0)
21
+ ![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)![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-dark.svg?fit=max&auto=format&n=x7pO8l4XcvAXCoVc&q=85&s=c9b3d88487335f58cce0b52e2f9e7531)
22
22
 
23
23
  The table below summarizes when each event fires. The [Hook events](https://code.claude.com/docs/en/hooks#hook-events) section documents the full input schema and decision control options for each one.
24
24
 
@@ -26,11 +26,11 @@ The table below summarizes when each event fires. The [Hook events](https://code
26
26
  | --- | --- |
27
27
  | `SessionStart` | When a session begins or resumes |
28
28
  | `Setup` | When you start Claude Code with `--init-only`, or with `--init` or `--maintenance` in `-p` mode. For one-time preparation in CI or scripts |
29
- | `UserPromptSubmit` | When you submit a prompt, before Claude processes it |
29
+ | `UserPromptSubmit` | When a prompt is submitted, before Claude processes it. Also fires on [turns Claude Code starts on its own](https://code.claude.com/docs/en/hooks#userpromptsubmit) |
30
30
  | `UserPromptExpansion` | When a user-typed command expands into a prompt, before it reaches Claude. Can block the expansion |
31
31
  | `PreToolUse` | Before a tool call executes. Can block it |
32
- | `PermissionRequest` | When a permission dialog appears |
33
- | `PermissionDenied` | When a tool call is denied by the auto mode classifier. Return `{retry: true}` to tell the model it may retry the denied tool call |
32
+ | `PermissionRequest` | When a tool call needs a permission decision |
33
+ | `PermissionDenied` | When auto mode denies a tool call, including denials without a classifier verdict. Use JSON `hookSpecificOutput.retry: true` to tell the model it may retry the denied tool call. Claude Code ignores `retry` when the classifier produced no verdict |
34
34
  | `PostToolUse` | After a tool call succeeds |
35
35
  | `PostToolUseFailure` | After a tool call fails |
36
36
  | `PostToolBatch` | After a full batch of parallel tool calls resolves, before the next model call |
@@ -41,23 +41,33 @@ The table below summarizes when each event fires. The [Hook events](https://code
41
41
  | `TaskCreated` | When a task is being created via `TaskCreate` |
42
42
  | `TaskCompleted` | When a task is being marked as completed |
43
43
  | `Stop` | When Claude finishes responding |
44
- | `StopFailure` | When the turn ends due to an API error. Output and exit code are ignored |
44
+ | `StopFailure` | When the turn ends due to an API error |
45
45
  | `TeammateIdle` | When an [agent team](https://code.claude.com/docs/en/agent-teams) teammate is about to go idle |
46
46
  | `InstructionsLoaded` | When a CLAUDE.md or `.claude/rules/*.md` file is loaded into context. Fires at session start and when files are lazily loaded during a session |
47
47
  | `ConfigChange` | When a configuration file changes during a session |
48
48
  | `CwdChanged` | When the working directory changes, for example when Claude executes a `cd` command. Useful for reactive environment management with tools like direnv |
49
+ | `DirectoryAdded` | When a working directory is added mid-session via `/add-dir` or the SDK `register_repo_root` control request |
49
50
  | `FileChanged` | When a watched file changes on disk. The `matcher` field specifies which filenames to watch |
50
- | `WorktreeCreate` | When a worktree is being created via `--worktree` or `isolation: "worktree"`. Replaces default git behavior |
51
- | `WorktreeRemove` | When a worktree is being removed, either at session exit or when a subagent finishes |
51
+ | `WorktreeCreate` | When a worktree is being created via `--worktree`, `isolation: "worktree"`, or for a background session. Replaces default git behavior |
52
+ | `WorktreeRemove` | When a worktree that a `WorktreeCreate` hook created is being removed |
52
53
  | `PreCompact` | Before context compaction |
53
54
  | `PostCompact` | After context compaction completes |
55
+ | `PreModelSwitch` | Before Claude Code applies a model switch that you or a client requested. Can block the switch |
56
+ | `PostModelSwitch` | After the session’s model changes, including changes Claude Code makes on its own, such as restoring the model when you resume a session |
54
57
  | `Elicitation` | When an MCP server requests user input during a tool call |
55
58
  | `ElicitationResult` | After a user responds to an MCP elicitation, before the response is sent back to the server |
56
59
  | `SessionEnd` | When a session terminates |
57
60
 
58
61
  ### [​](https://code.claude.com/docs/en/hooks\#how-a-hook-resolves) How a hook resolves
59
62
 
60
- To see how these pieces fit together, consider this `PreToolUse` hook that blocks destructive shell commands. 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:
63
+ To see how the event, the matcher, and the handler fit together, consider this `PreToolUse` hook that blocks destructive shell commands.
64
+
65
+ - macOS/Linux
66
+
67
+ - Windows (PowerShell)
68
+
69
+
70
+ 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:
61
71
 
62
72
  ```
63
73
  {
@@ -79,7 +89,7 @@ To see how these pieces fit together, consider this `PreToolUse` hook that block
79
89
  }
80
90
  ```
81
91
 
82
- The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf`:
92
+ 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:
83
93
 
84
94
  ```
85
95
  #!/bin/bash
@@ -99,13 +109,73 @@ else
99
109
  fi
100
110
  ```
101
111
 
102
- Now suppose Claude Code decides to run `Bash "rm -rf /tmp/build"`. Here’s what happens:
112
+ 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.
103
113
 
104
- ![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)
114
+ The matcher `Bash|PowerShell` covers the [PowerShell tool](https://code.claude.com/docs/en/hooks#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`:
105
115
 
106
- 1
116
+ ```
117
+ {
118
+ "hooks": {
119
+ "PreToolUse": [\
120
+ {\
121
+ "matcher": "Bash|PowerShell",\
122
+ "hooks": [\
123
+ {\
124
+ "type": "command",\
125
+ "if": "Bash(rm *)",\
126
+ "command": "powershell.exe",\
127
+ "args": [\
128
+ "-NoProfile",\
129
+ "-ExecutionPolicy",\
130
+ "Bypass",\
131
+ "-File",\
132
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"\
133
+ ]\
134
+ },\
135
+ {\
136
+ "type": "command",\
137
+ "if": "PowerShell(Remove-Item *)",\
138
+ "command": "powershell.exe",\
139
+ "args": [\
140
+ "-NoProfile",\
141
+ "-ExecutionPolicy",\
142
+ "Bypass",\
143
+ "-File",\
144
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"\
145
+ ]\
146
+ }\
147
+ ]\
148
+ }\
149
+ ]
150
+ }
151
+ }
152
+ ```
153
+
154
+ The `-NoProfile` flag skips loading your PowerShell profile so the hook starts fast, and `-ExecutionPolicy Bypass` lets PowerShell run the local script file.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:
155
+
156
+ ```
157
+ # .claude/hooks/block-rm.ps1
158
+ $callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
159
+ $command = $callInput.tool_input.command
160
+
161
+ if ($command -match 'rm -rf|Remove-Item.*-Recurse') {
162
+ @{
163
+ hookSpecificOutput = @{
164
+ hookEventName = "PreToolUse"
165
+ permissionDecision = "deny"
166
+ permissionDecisionReason = "Destructive command blocked by hook"
167
+ }
168
+ } | ConvertTo-Json
169
+ } else {
170
+ exit 0 # no decision; normal permission flow applies
171
+ }
172
+ ```
173
+
174
+ Now suppose Claude Code decides to run `Bash "rm -rf /tmp/build"` against the macOS/Linux config. Here’s what happens:
107
175
 
108
- [Navigate to header](https://code.claude.com/docs/en/hooks#)
176
+ ![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)![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/_xqph1dUOslCOwsj/images/hook-resolution-dark.svg?fit=max&auto=format&n=_xqph1dUOslCOwsj&q=85&s=e80af91f8507cee6bd51ac3c2dd92f63)
177
+
178
+ 1
109
179
 
110
180
  Event fires
111
181
 
@@ -117,24 +187,18 @@ The `PreToolUse` event fires. Claude Code sends the tool input as JSON on stdin
117
187
 
118
188
  2
119
189
 
120
- [Navigate to header](https://code.claude.com/docs/en/hooks#)
121
-
122
190
  Matcher checks
123
191
 
124
192
  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.
125
193
 
126
194
  3
127
195
 
128
- [Navigate to header](https://code.claude.com/docs/en/hooks#)
129
-
130
196
  If condition checks
131
197
 
132
198
  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.
133
199
 
134
200
  4
135
201
 
136
- [Navigate to header](https://code.claude.com/docs/en/hooks#)
137
-
138
202
  Hook handler runs
139
203
 
140
204
  The script inspects the full command and finds `rm -rf`, so it prints a decision to stdout:
@@ -153,8 +217,6 @@ If the command had been a safer `rm` variant like `rm file.txt`, the script woul
153
217
 
154
218
  5
155
219
 
156
- [Navigate to header](https://code.claude.com/docs/en/hooks#)
157
-
158
220
  Claude Code acts on the result
159
221
 
160
222
  Claude Code reads the JSON decision, blocks the tool call, and shows Claude the reason.
@@ -181,12 +243,23 @@ Where you define a hook determines its scope:
181
243
  | --- | --- | --- |
182
244
  | `~/.claude/settings.json` | All your projects | No, local to your machine |
183
245
  | `.claude/settings.json` | Single project | Yes, can be committed to the repo |
184
- | `.claude/settings.local.json` | Single project | No, gitignored when Claude Code creates it |
246
+ | `.claude/settings.local.json` | Single project | No, gitignored when Claude Code saves a setting to it |
185
247
  | Managed policy settings | Organization-wide | Yes, admin-controlled |
186
- | [Plugin](https://code.claude.com/docs/en/plugins)`hooks/hooks.json` | When plugin is enabled | Yes, bundled with the plugin |
187
- | [Skill](https://code.claude.com/docs/en/skills) or [agent](https://code.claude.com/docs/en/sub-agents) frontmatter | While the component is active | Yes, defined in the component file |
248
+ | [Plugin](https://code.claude.com/docs/en/plugins/overview)`hooks/hooks.json` | When plugin is enabled | Yes, bundled with the plugin |
249
+ | [Skill](https://code.claude.com/docs/en/skills) frontmatter | The rest of the session once the skill is invoked. See [Hooks in skills and agents](https://code.claude.com/docs/en/hooks#hooks-in-skills-and-agents) | Yes, defined in the skill file |
250
+ | [Subagent](https://code.claude.com/docs/en/sub-agents) frontmatter | While that subagent is running | Yes, defined in the subagent file |
251
+
252
+ [Cloud sessions](https://code.claude.com/docs/en/claude-code-on-the-web) don’t read your local `~/.claude/settings.json`. In a [self-hosted environment](https://code.claude.com/docs/en/self-hosted-environments-configuration#permissions-and-tool-approval), Claude Code also runs the hooks the operator seeded from the runner host’s `~/.claude/`, and it runs the hooks in the runner image’s managed settings file when that file is among the [managed sources Claude Code applies](https://code.claude.com/docs/en/managed-settings#how-claude-code-combines-managed-sources), which by default means only when neither server-managed settings nor an MDM-delivered Claude Code policy supplies the managed tier. See [what carries over from your setup](https://code.claude.com/docs/en/cloud-environments#what-carries-over-from-your-setup) for which settings files and plugins, and so which hooks, reach a cloud session.For details on settings file resolution, see [settings](https://code.claude.com/docs/en/settings).Hooks from settings files, managed policy settings, and plugins also run inside [subagents](https://code.claude.com/docs/en/sub-agents). When a subagent calls a tool, tool events such as `PreToolUse` and `PostToolUse` fire the same configured hooks as in the main conversation, and the input carries the `agent_id` and `agent_type` [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields) that identify the subagent.Administrators can use [`allowManagedHooksOnly`](https://code.claude.com/docs/en/settings-reference#allowmanagedhooksonly) in [managed settings](https://code.claude.com/docs/en/managed-settings) to restrict which hooks run:
253
+
254
+ - Your user, project, local, and plugin hooks are blocked. Hooks from plugins force-enabled in managed settings `enabledPlugins` are exempt
255
+ - Claude Code also narrows your [`statusLine`](https://code.claude.com/docs/en/statusline), [`fileSuggestion`](https://code.claude.com/docs/en/settings-reference#filesuggestion), and [`subagentStatusLine`](https://code.claude.com/docs/en/statusline#subagent-status-lines) settings to managed settings
256
+ - Claude Code also disables plugins with a [`command` source](https://code.claude.com/docs/en/plugins/marketplace-reference#command-plugin-source), including plugins force-enabled in managed settings `enabledPlugins`, unless [`disableCommandPluginSources`](https://code.claude.com/docs/en/settings-reference#disablecommandpluginsources) is explicitly set to `false`. `command` sources require Claude Code v2.1.229 or later
257
+ - Claude Code also blocks marketplace [`headersHelper` commands](https://code.claude.com/docs/en/plugins/host-marketplace#authenticate-archive-downloads) unless [`disableCommandPluginSources`](https://code.claude.com/docs/en/settings-reference#disablecommandpluginsources) is explicitly set to `false`, except for a marketplace that managed settings themselves declare
188
258
 
189
- For details on settings file resolution, see [settings](https://code.claude.com/docs/en/settings).Enterprise administrators can use `allowManagedHooksOnly` to block user, project, and plugin hooks. Hooks from plugins force-enabled in managed settings `enabledPlugins` are exempt, so administrators can distribute vetted hooks through an organization marketplace. See [Hook configuration](https://code.claude.com/docs/en/settings#hook-configuration).
259
+ See [what runs under `allowManagedHooksOnly`](https://code.claude.com/docs/en/settings-reference#what-runs-under-allowmanagedhooksonly).Hook entries merge across settings levels rather than replacing each other: user, project, and local settings add their own hooks without removing managed ones, and the [`disableAllHooks`](https://code.claude.com/docs/en/hooks#disable-or-remove-hooks) setting can’t disable managed hooks from outside managed settings.The [HTTP hook allowlists](https://code.claude.com/docs/en/settings-reference#hook-and-skill-settings) apply to hooks from every source, including managed policy settings:
260
+
261
+ - `allowedHttpHookUrls`: when defined at any settings level, Claude Code runs an HTTP hook handler only if its URL matches the merged allowlist
262
+ - `httpHookAllowedEnvVars`: when defined, Claude Code interpolates only the environment variables on that list into hook headers
190
263
 
191
264
  ### [​](https://code.claude.com/docs/en/hooks\#matcher-patterns) Matcher patterns
192
265
 
@@ -198,29 +271,31 @@ The `matcher` field filters when hooks fire. How a matcher is evaluated depends
198
271
  | Only letters, digits, `_`, `-`, spaces, `,`, and `|` | Exact string, or list of exact strings separated by `|` or `,` with optional surrounding whitespace | `Bash` matches only the Bash tool; `Edit|Write` and `Edit, Write` each match either tool exactly; `code-reviewer` matches only that agent type |
199
272
  | Contains any other character | JavaScript regular expression, unanchored | `^Notebook` matches any tool whose name starts with `Notebook`; `mcp__memory__.*` matches every tool from the `memory` server |
200
273
 
201
- A matcher on the regular-expression path is tested with JavaScript’s `RegExp.prototype.test`, which succeeds on a match anywhere in the value. `Edit.*` matches both `Edit` and `NotebookEdit`; wrap the pattern in `^` and `$`, as in `^Edit$`, when you need a whole-string match.Comma separators and the surrounding whitespace tolerance require Claude Code v2.1.191 or later.Hyphens in the exact-match set require Claude Code v2.1.195 or later. On earlier versions a hyphenated name like `code-reviewer` is evaluated as an unanchored regular expression, so it also fires for `senior-code-reviewer`; anchor it as `^code-reviewer$` on those versions to match only that name.`FileChanged` and `StopFailure` use a narrower exact-match set of letters, digits, `_`, and `|` only. A hyphen, space, or comma in a matcher for those two events keeps it on the regular-expression path, and only `|` separates alternatives. Every other event with matcher support in the table that follows accepts `|` or `,`.The `FileChanged` event doesn’t follow these rules when building its watch list. See [FileChanged](https://code.claude.com/docs/en/hooks#filechanged).Each event type matches on a different field:
274
+ A matcher on the regular-expression path is tested with JavaScript’s `RegExp.prototype.test`, which succeeds on a match anywhere in the value. `Edit.*` matches both `Edit` and `NotebookEdit`; wrap the pattern in `^` and `$`, as in `^Edit$`, when you need a whole-string match.`FileChanged` and `StopFailure` use a narrower exact-match set of letters, digits, `_`, and `|` only. A hyphen, space, or comma in a matcher for those two events keeps it on the regular-expression path, and only `|` separates alternatives. Every other event with matcher support in the table that follows accepts `|` or `,`.The `FileChanged` event doesn’t follow these rules when building its watch list. See [FileChanged](https://code.claude.com/docs/en/hooks#filechanged).Each event type matches on a different field:
202
275
 
203
276
  | Event | What the matcher filters | Example matcher values |
204
277
  | --- | --- | --- |
205
278
  | `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied` | tool name | `Bash`, `Edit|Write`, `mcp__.*` |
206
- | `SessionStart` | how the session started | `startup`, `resume`, `clear`, `compact` |
279
+ | `SessionStart` | how the session started | `startup`, `resume`, `clear`, `compact`, `fork` |
207
280
  | `Setup` | which CLI flag triggered setup | `init`, `maintenance` |
208
- | `SessionEnd` | why the session ended | `clear`, `resume`, `logout`, `prompt_input_exit`, `bypass_permissions_disabled`, `other` |
209
- | `Notification` | notification type | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`, `elicitation_complete`, `elicitation_response`, `agent_needs_input`, `agent_completed` |
281
+ | `SessionEnd` | why the session ended | `clear`, `resume`, `logout`, `prompt_input_exit`, `other` |
282
+ | `Notification` | notification type | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`, `elicitation_url_dialog`, `elicitation_complete`, `elicitation_response`, `agent_needs_input`, `agent_completed`, `quota_auto_resume_fired`, `quota_auto_resume_stale`, `quota_auto_resume_disabled` |
210
283
  | `SubagentStart` | agent type | `general-purpose`, `Explore`, `Plan`, custom agent names, or plugin-scoped names like `^my-plugin:reviewer$` |
211
284
  | `PreCompact`, `PostCompact` | what triggered compaction | `manual`, `auto` |
285
+ | `PreModelSwitch`, `PostModelSwitch` | canonical name of the model the session switches to, as described under [PreModelSwitch](https://code.claude.com/docs/en/hooks#premodelswitch) | `claude-opus-5`, `claude-opus-4-6|claude-opus-5`, `.*opus.*` |
212
286
  | `SubagentStop` | agent type | same values as `SubagentStart` |
213
287
  | `ConfigChange` | configuration source | `user_settings`, `project_settings`, `local_settings`, `policy_settings`, `skills` |
214
- | `CwdChanged` | no matcher support | always fires on every directory change |
288
+ | `CwdChanged` | no matcher support | always fires on every occurrence |
289
+ | `DirectoryAdded` | how the directory was added | `slash_command`, `register_repo_root` |
215
290
  | `FileChanged` | literal filenames to watch (see [FileChanged](https://code.claude.com/docs/en/hooks#filechanged)) | `.envrc|.env` |
216
- | `StopFailure` | error type | `rate_limit`, `overloaded`, `authentication_failed`, `oauth_org_not_allowed`, `billing_error`, `invalid_request`, `model_not_found`, `server_error`, `max_output_tokens`, `unknown` |
291
+ | `StopFailure` | error type | `rate_limit`, `overloaded`, `authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `billing_error`, `invalid_request`, `model_not_found`, `server_error`, `max_output_tokens`, `cloud_credential_error`, `unknown` |
217
292
  | `InstructionsLoaded` | load reason | `session_start`, `nested_traversal`, `path_glob_match`, `include`, `compact` |
218
293
  | `UserPromptExpansion` | command name | your skill or command names |
219
294
  | `Elicitation` | MCP server name | your configured MCP server names |
220
295
  | `ElicitationResult` | MCP server name | same values as `Elicitation` |
221
296
  | `UserPromptSubmit`, `PostToolBatch`, `Stop`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove`, `MessageDisplay` | no matcher support | always fires on every occurrence |
222
297
 
223
- The matcher runs against a field from the [JSON input](https://code.claude.com/docs/en/hooks#hook-input-and-output) that Claude Code sends to your hook on stdin. For tool events, that field is `tool_name`. Each [hook event](https://code.claude.com/docs/en/hooks#hook-events) section lists the full set of matcher values and the input schema for that event.This example runs a linting script only when Claude writes or edits a file:
298
+ Matching `StopFailure` on `cloud_credential_error` requires Claude Code v2.1.267 or later, the first version that reports credential-load failures under that value rather than `server_error` or `unknown`.For most events, Claude Code evaluates the matcher against a field from the [JSON input](https://code.claude.com/docs/en/hooks#hook-input-and-output) it sends to your hook on stdin. For tool events, that field is `tool_name`. For `PreModelSwitch` and `PostModelSwitch`, Claude Code evaluates the matcher against the canonical name it derives from `to_model`, as described under [PreModelSwitch](https://code.claude.com/docs/en/hooks#premodelswitch). Each [hook event](https://code.claude.com/docs/en/hooks#hook-events) section lists the full set of matcher values and the input schema for that event.This example runs a linting script only when Claude writes or edits a file:
224
299
 
225
300
  ```
226
301
  {
@@ -240,7 +315,7 @@ The matcher runs against a field from the [JSON input](https://code.claude.com/d
240
315
  }
241
316
  ```
242
317
 
243
- `UserPromptSubmit`, `PostToolBatch`, `Stop`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove`, `MessageDisplay`, and `CwdChanged` don’t support matchers and always fire on every occurrence. If you add a `matcher` field to these events, it is silently ignored.For tool events, you can filter more narrowly by setting the [`if` field](https://code.claude.com/docs/en/hooks#common-fields) on individual hook handlers. `if` uses [permission rule syntax](https://code.claude.com/docs/en/permissions) to match against the tool name and arguments together, so `"Bash(git *)"` runs when any subcommand of the Bash input matches `git *` and `"Edit(*.ts)"` runs only for TypeScript files.
318
+ If you add a `matcher` field to an event without matcher support, it is silently ignored.For tool events, you can filter more narrowly by setting the [`if` field](https://code.claude.com/docs/en/hooks#common-fields) on individual hook handlers. `if` uses [permission rule syntax](https://code.claude.com/docs/en/permissions) to match against the tool name and arguments together, so `"Bash(git *)"` runs when any subcommand of the Bash input matches `git *` and `"Edit(*.ts)"` runs only for TypeScript files.
244
319
 
245
320
  #### [​](https://code.claude.com/docs/en/hooks\#match-mcp-tools) Match MCP tools
246
321
 
@@ -256,7 +331,7 @@ To match every tool from a server, append `.*` to the server prefix. The `.*` is
256
331
  - `mcp__brave-search__.*` matches all tools from a server whose name contains a hyphen
257
332
  - `mcp__.*__write.*` matches any tool whose name starts with `write` from any server
258
333
 
259
- Hyphens in the exact-match set require Claude Code v2.1.195 or later. On earlier versions a bare hyphenated prefix like `mcp__brave-search` is evaluated as an unanchored regular expression and matches every tool from that server. The `mcp__brave-search__.*` form works on every version.Tools from a [plugin-bundled MCP server](https://code.claude.com/docs/en/mcp#plugin-provided-mcp-servers) use a scoped server segment that includes the plugin name: `mcp__plugin_<plugin-name>_<server-name>__<tool>`. A matcher written against the bare server key never fires for these tools. For a plugin named `my-plugin` that bundles a server under the key `db`, a `query` tool appears as `mcp__plugin_my-plugin_db__query`, so the matcher for every tool from that server is `mcp__plugin_my-plugin_db__.*`. Use the same scoped tool name in a handler’s [`if` field](https://code.claude.com/docs/en/hooks#common-fields). See [Plugin-provided MCP servers](https://code.claude.com/docs/en/mcp#plugin-provided-mcp-servers) for how the scoped name is built.This example logs all memory server operations and validates write operations from any MCP server:
334
+ Tools from a [plugin-bundled MCP server](https://code.claude.com/docs/en/mcp#plugin-provided-mcp-servers) use a scoped server segment that includes the plugin name: `mcp__plugin_<plugin-name>_<server-name>__<tool>`. A matcher written against the bare server key never fires for these tools. For a plugin named `my-plugin` that bundles a server under the key `db`, a `query` tool appears as `mcp__plugin_my-plugin_db__query`, so the matcher for every tool from that server is `mcp__plugin_my-plugin_db__.*`. Use the same scoped tool name in a handler’s [`if` field](https://code.claude.com/docs/en/hooks#common-fields). See [Plugin-provided MCP servers](https://code.claude.com/docs/en/mcp#plugin-provided-mcp-servers) for how the scoped name is built.This example logs all memory server operations and validates write operations from any MCP server:
260
335
 
261
336
  ```
262
337
  {
@@ -291,11 +366,11 @@ Each object in the inner `hooks` array is a hook handler: the shell command, HTT
291
366
 
292
367
  - **[Command hooks](https://code.claude.com/docs/en/hooks#command-hook-fields)** (`type: "command"`): run a shell command. Your script receives the event’s [JSON input](https://code.claude.com/docs/en/hooks#hook-input-and-output) on stdin and communicates results back through exit codes and stdout.
293
368
  - **[HTTP hooks](https://code.claude.com/docs/en/hooks#http-hook-fields)** (`type: "http"`): send the event’s JSON input as an HTTP POST request to a URL. The endpoint communicates results back through the response body using the same [JSON output format](https://code.claude.com/docs/en/hooks#json-output) as command hooks.
294
- - **[MCP tool hooks](https://code.claude.com/docs/en/hooks#mcp-tool-hook-fields)** (`type: "mcp_tool"`): call a tool on an already-connected [MCP server](https://code.claude.com/docs/en/mcp). The tool’s text output is treated like command-hook stdout.
295
- - **[Prompt hooks](https://code.claude.com/docs/en/hooks#prompt-and-agent-hook-fields)** (`type: "prompt"`): send a prompt to a Claude model for single-turn evaluation. The model returns a yes/no decision as JSON. See [Prompt-based hooks](https://code.claude.com/docs/en/hooks#prompt-based-hooks).
369
+ - **[MCP tool hooks](https://code.claude.com/docs/en/hooks#mcp-tool-hook-fields)** (`type: "mcp_tool"`): call a tool on a configured [MCP server](https://code.claude.com/docs/en/mcp). The tool’s text output is treated like command-hook stdout.
370
+ - **[Prompt hooks](https://code.claude.com/docs/en/hooks#prompt-and-agent-hook-fields)** (`type: "prompt"`): send a prompt to a Claude model for single-turn evaluation. The model returns its decision as JSON. See [Prompt-based hooks](https://code.claude.com/docs/en/hooks#prompt-based-hooks).
296
371
  - **[Agent hooks](https://code.claude.com/docs/en/hooks#prompt-and-agent-hook-fields)** (`type: "agent"`): spawn a subagent that can use tools like Read, Grep, and Glob to verify conditions before returning a decision. Agent hooks are experimental and may change. See [Agent-based hooks](https://code.claude.com/docs/en/hooks#agent-based-hooks).
297
372
 
298
- All matching hooks run in parallel, and identical handlers are deduplicated automatically. Command hooks are deduplicated by command string and `args`, and HTTP hooks are deduplicated by URL.Handlers run in the current directory with Claude Code’s environment. The `$CLAUDE_CODE_REMOTE` environment variable is set to `"true"` in remote web environments and not set in the local CLI. As of v2.1.199, [`$CLAUDE_CODE_BRIDGE_SESSION_ID`](https://code.claude.com/docs/en/env-vars) is set to the [Remote Control](https://code.claude.com/docs/en/remote-control) session ID while the local session has an active Remote Control connection.
373
+ All matching hooks run in parallel. If you define the same handler in more than one settings file, it runs once. A plugin’s or skill’s copy of the same handler stays separate.Handlers run in the current directory with Claude Code’s environment. If the current directory no longer exists, for example a worktree or temp directory that another shell deleted mid-session, Claude Code runs command hooks from the first of these that still exists: the directory the session started in, the project root, your home directory, or the system temp directory. Claude Code records a warning naming the fallback directory in the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks). For a worktree session that you start from the desktop app, see [What worktrees share with the main checkout](https://code.claude.com/docs/en/worktrees#what-worktrees-share-with-the-main-checkout).The `$CLAUDE_CODE_REMOTE` environment variable is `"true"` in remote web environments and not set in the local CLI. Claude Code v2.1.199 and later sets [`$CLAUDE_CODE_BRIDGE_SESSION_ID`](https://code.claude.com/docs/en/env-vars) to the [Remote Control](https://code.claude.com/docs/en/remote-control) session ID while the local session has an active Remote Control connection.
299
374
 
300
375
  #### [​](https://code.claude.com/docs/en/hooks\#common-fields) Common fields
301
376
 
@@ -304,12 +379,16 @@ These fields apply to all hook types:
304
379
  | Field | Required | Description |
305
380
  | --- | --- | --- |
306
381
  | `type` | yes | `"command"`, `"http"`, `"mcp_tool"`, `"prompt"`, or `"agent"` |
307
- | `if` | no | Permission rule syntax to filter when this hook runs, such as `"Bash(git *)"` or `"Edit(*.ts)"`. The hook command only runs if the tool call matches the pattern. See the [Bash matching table](https://code.claude.com/docs/en/hooks#bash-if-matching) below for how Bash patterns evaluate against subcommands, `$()`, and backticks. Only evaluated on tool events: `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, and `PermissionDenied`. On other events, a hook with `if` set never runs. Uses the same syntax as [permission rules](https://code.claude.com/docs/en/permissions) |
308
- | `timeout` | no | Seconds before canceling. Defaults: 600 for `command`, `http`, and `mcp_tool`; 30 for `prompt`; 60 for `agent`. [`UserPromptSubmit`](https://code.claude.com/docs/en/hooks#userpromptsubmit) lowers the `command`, `http`, and `mcp_tool` default to 30, and [`MessageDisplay`](https://code.claude.com/docs/en/hooks#messagedisplay) lowers it to 10 |
382
+ | `if` | no | [Permission rule syntax](https://code.claude.com/docs/en/permissions#permission-rule-syntax) to filter when this hook runs, such as `"Bash(git *)"` or `"Edit(*.ts)"`. The hook command only runs if the tool call matches the pattern. See the [Bash matching table](https://code.claude.com/docs/en/hooks#bash-if-matching) for how Bash patterns evaluate against subcommands, `$()`, and backticks. Only evaluated on tool events: `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, and `PermissionDenied`. On other events, a hook with `if` set never runs |
383
+ | `timeout` | no | Seconds before canceling. Claude Code doesn’t enforce it on a command hook you run with [`async: true`](https://code.claude.com/docs/en/hooks#run-hooks-in-the-background). Defaults: 600 for `command`, `http`, and `mcp_tool`; 30 for `prompt`; 60 for `agent`. Claude Code lowers the `command`, `http`, and `mcp_tool` default to 30 on [`UserPromptSubmit`](https://code.claude.com/docs/en/hooks#userpromptsubmit), [`PreModelSwitch`](https://code.claude.com/docs/en/hooks#premodelswitch), and [`PostModelSwitch`](https://code.claude.com/docs/en/hooks#postmodelswitch), and to 10 on [`MessageDisplay`](https://code.claude.com/docs/en/hooks#messagedisplay). [`SessionEnd`](https://code.claude.com/docs/en/hooks#sessionend) hooks share a 1.5-second budget; if your settings set a longer per-hook `timeout`, Claude Code raises the budget to match, up to 60 seconds |
309
384
  | `statusMessage` | no | Custom spinner message displayed while the hook runs |
310
- | `once` | no | If `true`, runs once per session then is removed. Only honored for hooks declared in [skill frontmatter](https://code.claude.com/docs/en/hooks#hooks-in-skills-and-agents); ignored in settings files and agent frontmatter |
385
+ | `once` | no | If `true`, Claude Code removes the hook after its first successful run. A run that fails, blocks with exit code 2, or times out leaves the hook in place, so it runs again on the next matching event. Only honored for hooks declared in [skill frontmatter](https://code.claude.com/docs/en/hooks#hooks-in-skills-and-agents); ignored in settings files and agent frontmatter |
386
+
387
+ The `if` field holds exactly one permission rule. There is no `&&`, `||`, or list syntax for combining rules; to apply multiple conditions, define a separate hook handler for each.In 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/**)"`. Before v2.1.214, `"Edit(src/**)"` matched a directory named `src` at any depth under the working directory.
311
388
 
312
- The `if` field holds exactly one permission rule. There is no `&&`, `||`, or list syntax for combining rules; to apply multiple conditions, define a separate hook handler for each.For Bash patterns, 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.
389
+ #### [​](https://code.claude.com/docs/en/hooks\#bash-if-matching) How `if` patterns match Bash commands
390
+
391
+ For Bash patterns in the [`if` field](https://code.claude.com/docs/en/hooks#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.
313
392
 
314
393
  | `if` pattern | Bash command | Hook runs? | Why |
315
394
  | --- | --- | --- | --- |
@@ -319,7 +398,7 @@ The `if` field holds exactly one permission rule. There is no `&&`, `||`, or lis
319
398
  | `Bash(rm *)` | `echo $(date)` | no | no subcommand matches `rm *` |
320
399
  | `Bash(git push *)` | `echo $(date)` | yes | patterns that specify more than the command name run the hook anyway on `$()`, backticks, or `$VAR` |
321
400
 
322
- The filter also fails open, running your hook regardless of pattern, when the Bash command can’t be parsed. Because the `if` filter is best-effort, use the [permission system](https://code.claude.com/docs/en/permissions) rather than a hook to enforce a hard allow or deny.
401
+ When Claude Code can’t determine which commands the Bash input runs, it runs your hook regardless of the pattern. Because the `if` filter is best-effort, use the [permission system](https://code.claude.com/docs/en/permissions) rather than a hook to enforce a hard allow or deny.
323
402
 
324
403
  #### [​](https://code.claude.com/docs/en/hooks\#command-hook-fields) Command hook fields
325
404
 
@@ -330,8 +409,9 @@ In addition to the [common fields](https://code.claude.com/docs/en/hooks#common-
330
409
  | `command` | yes | Shell command to execute. With `args`, the executable to spawn directly. See [Exec form and shell form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form) |
331
410
  | `args` | no | Argument list. When present, `command` is resolved as an executable and spawned directly with `args` as the argument vector, with no shell involved. See [Exec form and shell form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form) |
332
411
  | `async` | no | If `true`, runs in the background without blocking. See [Run hooks in the background](https://code.claude.com/docs/en/hooks#run-hooks-in-the-background) |
333
- | `asyncRewake` | no | If `true`, runs in the background and wakes Claude on exit code 2. Implies `async`. The hook’s stderr, or stdout if stderr is empty, is shown to Claude as a system reminder so it can react to a long-running background failure |
412
+ | `asyncRewake` | no | If `true`, runs in the background and wakes Claude on exit code 2. The hook’s stderr, or stdout if stderr is empty, is shown to Claude as a [system reminder](https://code.claude.com/docs/en/glossary#system-reminder) so it can react to a long-running background failure |
334
413
  | `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 |
414
+ | `onFailure` | no | What happens to the action when the hook fails: `"continue"`, the default, or `"block"`. See [Block the action when a hook fails](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails). Requires Claude Code v2.1.295 or later |
335
415
 
336
416
  ##### Exec form and shell form
337
417
 
@@ -358,7 +438,7 @@ The equivalent shell form needs quoting to handle paths with spaces or special c
358
438
  }
359
439
  ```
360
440
 
361
- Both forms support the same [path placeholders](https://code.claude.com/docs/en/hooks#reference-scripts-by-path), and both export them as the environment variables `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, and `CLAUDE_PLUGIN_DATA` on the spawned process, so a script can read `process.env.CLAUDE_PLUGIN_ROOT` regardless of how it was launched. Plugin hooks additionally substitute `${user_config.*}` values; see [User configuration](https://code.claude.com/docs/en/plugins-reference#user-configuration).
441
+ Both forms support the same [path placeholders](https://code.claude.com/docs/en/hooks#reference-scripts-by-path), and both export them as the environment variables `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, and `CLAUDE_PLUGIN_DATA` on the spawned process, so a script can read `process.env.CLAUDE_PLUGIN_ROOT` regardless of how it was launched.Plugin hooks additionally substitute [`${user_config.*}`](https://code.claude.com/docs/en/plugins/manifest-reference#user-configuration) values, in exec form only: the value is substituted into `command` and into each `args` element as a plain string, so no shell re-parses it.A shell-form plugin hook whose `command` references `${user_config.*}` fails with an [error](https://code.claude.com/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.*}`.
362
442
 
363
443
  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.
364
444
 
@@ -371,8 +451,9 @@ In addition to the [common fields](https://code.claude.com/docs/en/hooks#common-
371
451
  | `url` | yes | URL to send the POST request to |
372
452
  | `headers` | no | Additional HTTP headers as key-value pairs. Values support environment variable interpolation using `$VAR_NAME` or `${VAR_NAME}` syntax. Only variables listed in `allowedEnvVars` are resolved |
373
453
  | `allowedEnvVars` | no | List of environment variable names that may be interpolated into header values. References to unlisted variables are replaced with empty strings. Required for any env var interpolation to work |
454
+ | `onFailure` | no | What happens to the action when the hook fails: `"continue"`, the default, or `"block"`. See [Block the action when a hook fails](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails). Requires Claude Code v2.1.295 or later |
374
455
 
375
- Claude Code sends the hook’s [JSON input](https://code.claude.com/docs/en/hooks#hook-input-and-output) as the POST request body with `Content-Type: application/json`. The response body uses the same [JSON output format](https://code.claude.com/docs/en/hooks#json-output) as command hooks.Error handling differs from command hooks: non-2xx responses, connection failures, and timeouts all produce non-blocking errors that allow execution to continue. To block a tool call or deny a permission, return a 2xx response with a JSON body containing `decision: "block"` or a `hookSpecificOutput` with `permissionDecision: "deny"`.This example sends `PreToolUse` events to a local validation service, authenticating with a token from the `MY_TOKEN` environment variable:
456
+ Claude Code sends the hook’s [JSON input](https://code.claude.com/docs/en/hooks#hook-input-and-output) as the POST request body with `Content-Type: application/json`. The response body uses the same [JSON output format](https://code.claude.com/docs/en/hooks#json-output) as command hooks.Error handling differs from command hooks; see [HTTP response handling](https://code.claude.com/docs/en/hooks#http-response-handling).This example sends `PreToolUse` events to a local validation service, authenticating with a token from the `MY_TOKEN` environment variable:
376
457
 
377
458
  ```
378
459
  {
@@ -403,11 +484,11 @@ In addition to the [common fields](https://code.claude.com/docs/en/hooks#common-
403
484
 
404
485
  | Field | Required | Description |
405
486
  | --- | --- | --- |
406
- | `server` | yes | Name of a configured MCP server. For a [plugin-bundled server](https://code.claude.com/docs/en/mcp#plugin-provided-mcp-servers), this is the scoped name `plugin:<plugin-name>:<server-name>`, such as `plugin:my-plugin:db`, not the bare server key. The server must already be connected; the hook never triggers an OAuth or connection flow |
487
+ | `server` | yes | Name of a configured MCP server. For a [plugin-bundled server](https://code.claude.com/docs/en/mcp#plugin-provided-mcp-servers), this is the scoped name `plugin:<plugin-name>:<server-name>`, such as `plugin:my-plugin:db`, not the bare server key |
407
488
  | `tool` | yes | Name of the tool to call on that server |
408
489
  | `input` | no | Arguments passed to the tool. String values support `${path}` substitution from the hook’s [JSON input](https://code.claude.com/docs/en/hooks#hook-input-and-output), such as `"${tool_input.file_path}"` |
409
490
 
410
- The tool’s text content is treated like command-hook stdout: if it parses as valid [JSON output](https://code.claude.com/docs/en/hooks#json-output) it is processed as a decision, otherwise it is shown as plain text. If the named server is not connected, or the tool returns `isError: true`, the hook produces a non-blocking error and execution continues.MCP tool hooks are available on every hook event once Claude Code has connected to your MCP servers. `SessionStart` and `Setup` typically fire before servers finish connecting, so hooks on those events should expect the “not connected” error on first run.This example calls the `security_scan` tool on the `my_server` MCP server after each `Write` or `Edit`, passing the edited file’s path:
491
+ This example calls the `security_scan` tool on the `my_server` MCP server after each `Write` or `Edit`, passing the edited file’s path:
411
492
 
412
493
  ```
413
494
  {
@@ -429,6 +510,18 @@ The tool’s text content is treated like command-hook stdout: if it parses as v
429
510
  }
430
511
  ```
431
512
 
513
+ ##### How the tool’s result is read
514
+
515
+ Claude Code reads the tool’s text content the same way it reads command-hook stdout, following the [parsing rule under exit code 0](https://code.claude.com/docs/en/hooks#exit-code-0). If the tool returns `isError: true`, the hook produces a non-blocking error and execution continues.
516
+
517
+ ##### When the server is still connecting
518
+
519
+ On events where a hook can block or change the result, such as `PreToolUse` or `Stop`, Claude Code waits for a connecting server before it calls the tool, for at most [`MCP_TIMEOUT`](https://code.claude.com/docs/en/env-vars) and within the hook’s own [`timeout`](https://code.claude.com/docs/en/hooks#common-fields). On observational events, such as `Notification` or `SessionEnd`, it doesn’t wait.A server showing the [`cached` status](https://code.claude.com/docs/en/mcp#server-status-detail) connects when the hook calls its tool. If the server isn’t connected at that point, the hook produces a non-blocking error and execution continues. The hook never starts an OAuth flow, so [authenticate the server from `/mcp`](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers) first.
520
+
521
+ ##### Events that fire before MCP servers are available
522
+
523
+ `SessionStart` at launch, including with `--continue` or `--resume`, and every `Setup` event fire before the session’s MCP servers are available to hooks. Claude Code skips their `mcp_tool` hooks without calling the tool, and the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks) records `mcp_tool hooks are not available for the 'SessionStart' hook event (no MCP client context)`, or the same message naming `Setup`. When `SessionStart` fires again later in the session, after `/clear` or a compaction, its `mcp_tool` hooks run. For anything the session needs at launch, use a `type: "command"` hook on `SessionStart` instead.
524
+
432
525
  #### [​](https://code.claude.com/docs/en/hooks\#prompt-and-agent-hook-fields) Prompt and agent hook fields
433
526
 
434
527
  In addition to the [common fields](https://code.claude.com/docs/en/hooks#common-fields), prompt and agent hooks accept these fields:
@@ -436,17 +529,24 @@ In addition to the [common fields](https://code.claude.com/docs/en/hooks#common-
436
529
  | Field | Required | Description |
437
530
  | --- | --- | --- |
438
531
  | `prompt` | yes | Prompt text to send to the model. Use `$ARGUMENTS` as a placeholder for the hook input JSON. Escape with a backslash to include literal text: `\$1.00` renders as `$1.00` |
439
- | `model` | no | Model to use for evaluation. Defaults to a fast model |
532
+ | `model` | no | Model to use for evaluation. Defaults to the model Claude Code uses for [background functionality](https://code.claude.com/docs/en/costs#background-token-usage) |
440
533
 
441
534
  ### [​](https://code.claude.com/docs/en/hooks\#reference-scripts-by-path) Reference scripts by path
442
535
 
443
536
  Use these placeholders to reference hook scripts relative to the project or plugin root, regardless of the working directory when the hook runs:
444
537
 
445
- - `${CLAUDE_PROJECT_DIR}`: the project root. Claude Code also sets this variable in the environment of [stdio MCP servers](https://code.claude.com/docs/en/mcp#option-3-add-a-local-stdio-server) and plugin LSP servers.
446
- - `${CLAUDE_PLUGIN_ROOT}`: the plugin’s installation directory, for scripts bundled with a [plugin](https://code.claude.com/docs/en/plugins). Changes on each plugin update.
447
- - `${CLAUDE_PLUGIN_DATA}`: the plugin’s [persistent data directory](https://code.claude.com/docs/en/plugins-reference#persistent-data-directory), for dependencies and state that should survive plugin updates.
538
+ - `${CLAUDE_PROJECT_DIR}`: the project root where the session started. Claude Code also sets this variable in the environment of [stdio MCP servers](https://code.claude.com/docs/en/mcp#option-3-add-a-local-stdio-server) and plugin LSP servers.
539
+ - `${CLAUDE_PLUGIN_ROOT}`: the plugin’s installation directory, for scripts bundled with a [plugin](https://code.claude.com/docs/en/plugins/overview). See [plugin environment variables](https://code.claude.com/docs/en/plugins/manifest-reference#environment-variables) for how the path behaves across updates.
540
+ - `${CLAUDE_PLUGIN_DATA}`: the plugin’s [persistent data directory](https://code.claude.com/docs/en/plugins/components#path-variables-and-persistent-data), for dependencies and state that should survive plugin updates.
541
+
542
+ **Worktrees are different.** If Claude enters a [worktree](https://code.claude.com/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:
543
+
544
+ - **`${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.
545
+ - **`cwd` follows Claude**: the `cwd` field in the hook’s [input JSON](https://code.claude.com/docs/en/hooks#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.
546
+
547
+ For a worktree session that you start from the desktop app, see [What worktrees share with the main checkout](https://code.claude.com/docs/en/worktrees#what-worktrees-share-with-the-main-checkout) for where `${CLAUDE_PROJECT_DIR}` points.
448
548
 
449
- Prefer [exec form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form) for any hook that references a path placeholder. Exec form passes each `args` element as one argument with no shell tokenization, so paths with spaces or special characters need no quoting. In shell form, wrap each placeholder in double quotes.
549
+ Prefer [exec form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form) for any hook that references a path placeholder. In shell form, wrap each placeholder in double quotes.
450
550
 
451
551
  - Project scripts
452
552
 
@@ -497,11 +597,16 @@ Define plugin hooks in `hooks/hooks.json` with an optional top-level `descriptio
497
597
  }
498
598
  ```
499
599
 
500
- See the [plugin components reference](https://code.claude.com/docs/en/plugins-reference#hooks) for details on creating plugin hooks.
600
+ See the [plugin components reference](https://code.claude.com/docs/en/plugins/components#hooks) for details on creating plugin hooks.
501
601
 
502
602
  ### [​](https://code.claude.com/docs/en/hooks\#hooks-in-skills-and-agents) Hooks in skills and agents
503
603
 
504
- In addition to settings files and plugins, hooks can be defined directly in [skills](https://code.claude.com/docs/en/skills) and [subagents](https://code.claude.com/docs/en/sub-agents) using frontmatter. These hooks are scoped to the component’s lifecycle and only run when that component is active.All hook events are supported. For subagents, `Stop` hooks are automatically converted to `SubagentStop` since that is the event that fires when a subagent completes.Hooks use the same configuration format as settings-based hooks but are scoped to the component’s lifetime and cleaned up when it finishes.This skill defines a `PreToolUse` hook that runs a security validation script before each `Bash` command:
604
+ In addition to settings files and plugins, hooks can be defined directly in [skills](https://code.claude.com/docs/en/skills) and [subagents](https://code.claude.com/docs/en/sub-agents) using frontmatter, in the same configuration format as settings-based hooks. How long Claude Code keeps them registered depends on the component:
605
+
606
+ - **Subagent hooks**: Claude Code runs them only while that subagent is running and removes them when it finishes. Claude Code converts a `Stop` hook here to `SubagentStop`, the event it fires when a subagent completes.
607
+ - **Skill hooks**: Claude Code registers them when you or Claude invoke the skill and keeps running them for the rest of the session, on turns after the skill’s own turn as well. To have Claude Code remove a hook after its first successful run instead, set [`once: true`](https://code.claude.com/docs/en/hooks#common-fields) on it.
608
+
609
+ This skill defines a `PreToolUse` hook that runs a security validation script before each `Bash` command:
505
610
 
506
611
  ```
507
612
  ---
@@ -516,28 +621,19 @@ hooks:
516
621
  ---
517
622
  ```
518
623
 
519
- Agents use the same format in their YAML frontmatter.
624
+ Subagents use the same format in their YAML frontmatter.Frontmatter hooks in a project skill follow the same [workspace trust rule as hooks in settings files](https://code.claude.com/docs/en/hooks#workspace-trust). Claude Code registers them when you or Claude invoke the skill, including in a `-p` run in a folder you haven’t trusted.Frontmatter hooks in a project subagent run only after you accept the [workspace trust dialog](https://code.claude.com/docs/en/permissions#project-allow-rules-and-workspace-trust) for the folder the agent file came from. A `-p` session doesn’t count as accepting it. [What runs before you trust a folder](https://code.claude.com/docs/en/permissions#what-runs-before-you-trust-a-folder) compares this with the settings-file rule, and the subagents page lists [which scopes are exempt](https://code.claude.com/docs/en/sub-agents#hooks-in-subagent-frontmatter). Before v2.1.218, these hooks could run from folders you hadn’t trusted.
520
625
 
521
626
  ### [​](https://code.claude.com/docs/en/hooks\#the-/hooks-menu) The `/hooks` menu
522
627
 
523
- Type `/hooks` in Claude Code to open a read-only browser for your configured hooks. The menu shows every hook event with a count of configured hooks, lets you drill into matchers, and shows the full details of each hook handler. Use it to verify configuration, check which settings file a hook came from, or inspect a hook’s command, prompt, or URL.The menu displays all five hook types: `command`, `prompt`, `agent`, `http`, and `mcp_tool`. Each hook is labeled with a `[type]` prefix and a source indicating where it was defined:
524
-
525
- - `User`: from `~/.claude/settings.json`
526
- - `Project`: from `.claude/settings.json`
527
- - `Local`: from `.claude/settings.local.json`
528
- - `Plugin`: from a plugin’s `hooks/hooks.json`
529
- - `Session`: registered in memory for the current session
530
- - `Built-in`: registered internally by Claude Code
531
-
532
- Selecting a hook opens a detail view showing its event, matcher, type, source file, and the full command, prompt, or URL. The menu is read-only: to add, modify, or remove hooks, edit the settings JSON directly or ask Claude to make the change.
628
+ Type `/hooks` in Claude Code to open a read-only browser for your configured hooks. The list labels each hook with where it comes from, such as user settings, project settings, local settings, a plugin, or the current session.Select a hook to see the full text of what it runs and where it’s defined, such as the path of its settings file or the name of its plugin.To browse all hook events, including ones with no hooks configured, select `All events` at the end of the list.
533
629
 
534
630
  ### [​](https://code.claude.com/docs/en/hooks\#disable-or-remove-hooks) Disable or remove hooks
535
631
 
536
- To remove a hook, delete its entry from the settings JSON file.To temporarily disable all hooks without removing them, set `"disableAllHooks": true` in your settings file. There is no way to disable an individual hook while keeping it in the configuration.The `disableAllHooks` setting respects the managed settings hierarchy. If an administrator has configured hooks through managed policy settings, `disableAllHooks` set in user, project, or local settings can’t disable those managed hooks. Only `disableAllHooks` set at the managed settings level can disable managed hooks.Direct edits to hooks in settings files are normally picked up automatically by the file watcher.
632
+ To remove a hook defined in a settings file, delete its entry from that file.To temporarily disable all hooks without removing them, set `"disableAllHooks": true` in your settings file. Claude Code reads the value left after [settings precedence](https://code.claude.com/docs/en/settings#settings-precedence) applies, so a `"disableAllHooks": false` in a project’s `.claude/settings.json` overrides a `true` in your user settings. To turn hooks off for one run whatever the project’s settings say, pass `--settings '{"disableAllHooks": true}'`, which takes precedence over project and local settings. There is no way to disable an individual hook while keeping it in the configuration.The `disableAllHooks` setting respects the managed settings hierarchy. If an administrator has configured hooks through managed policy settings, `disableAllHooks` set in user, project, or local settings can’t disable those managed hooks. Only `disableAllHooks` set at the managed settings level can disable managed hooks. For the full reach of each level, see [`disableAllHooks`](https://code.claude.com/docs/en/settings-reference#disableallhooks).Direct edits to hooks in settings files are normally picked up automatically by the file watcher.
537
633
 
538
634
  ## [​](https://code.claude.com/docs/en/hooks\#hook-input-and-output) Hook input and output
539
635
 
540
- Command hooks receive JSON data via stdin and communicate results through exit codes, stdout, and stderr. HTTP hooks receive the same JSON as the POST request body and communicate results through the HTTP response body. This section covers fields and behavior common to all events. Each event’s section under [Hook events](https://code.claude.com/docs/en/hooks#hook-events) includes its specific input schema and decision control options.On macOS and Linux, command hooks run in their own session without a controlling terminal as of v2.1.139. The hook process and any child processes can’t open `/dev/tty` or send escape sequences directly to the Claude Code interface. Windows has no `/dev/tty`. To surface a message to the user on any platform, return [`systemMessage`](https://code.claude.com/docs/en/hooks#json-output) in JSON output. To trigger a desktop notification, set a window title, or ring the bell, return [`terminalSequence`](https://code.claude.com/docs/en/hooks#emit-terminal-notifications) instead.
636
+ Command hooks receive JSON data via stdin and communicate results through exit codes, stdout, and stderr. HTTP hooks receive the same JSON as the POST request body and communicate results through the HTTP response body. This section covers fields and behavior common to all events. Each event’s section under [Hook events](https://code.claude.com/docs/en/hooks#hook-events) includes its specific input schema and decision control options.On macOS and Linux, command hooks run in their own session without a controlling terminal. The hook process and any child processes can’t open `/dev/tty` or send escape sequences directly to the Claude Code interface. Windows has no `/dev/tty`.To surface a message to the user on any platform, return [`systemMessage`](https://code.claude.com/docs/en/hooks#json-output) in JSON output. Some events discard it or deliver it elsewhere, and each [event’s section](https://code.claude.com/docs/en/hooks#hook-events) says so. To trigger a desktop notification, set a window title, or ring the bell, return [`terminalSequence`](https://code.claude.com/docs/en/hooks#emit-terminal-notifications) instead.
541
637
 
542
638
  ### [​](https://code.claude.com/docs/en/hooks\#common-input-fields) Common input fields
543
639
 
@@ -546,21 +642,22 @@ Hook events receive these fields as JSON, in addition to event-specific fields d
546
642
  | Field | Description |
547
643
  | --- | --- |
548
644
  | `session_id` | Current session identifier |
549
- | `prompt_id` | UUID identifying the user prompt currently being processed. Matches the [`prompt.id` attribute on OpenTelemetry events](https://code.claude.com/docs/en/monitoring-usage#event-correlation-attributes), so you can correlate hook output with telemetry for a single prompt. Absent until the first user input. Requires Claude Code v2.1.196 or later |
645
+ | `prompt_id` | UUID identifying the user prompt currently being processed. Matches the [`prompt.id` attribute on OpenTelemetry events](https://code.claude.com/docs/en/monitoring-usage#event-correlation-attributes), so you can correlate hook output with telemetry for a single prompt. Absent until the first user input |
550
646
  | `transcript_path` | Path to conversation JSON. The transcript file is written asynchronously and may lag the in-memory conversation, so it may not yet include the current turn’s most recent messages when a hook fires. Hooks that need the final assistant text of the current turn should use `last_assistant_message` on [Stop](https://code.claude.com/docs/en/hooks#stop) and [SubagentStop](https://code.claude.com/docs/en/hooks#subagentstop) instead of reading the transcript |
551
647
  | `cwd` | Current working directory when the hook is invoked |
648
+ | `scratchpad_dir` | Path to the session’s [scratchpad directory](https://code.claude.com/docs/en/claude-directory#session-scratchpad-directory), where Claude keeps temporary working files. Absent when the session has no scratchpad or the temp directory is unavailable. Requires Claude Code v2.1.257 or later |
552
649
  | `permission_mode` | Current [permission mode](https://code.claude.com/docs/en/permissions#permission-modes): `"default"`, `"plan"`, `"acceptEdits"`, `"auto"`, `"dontAsk"`, or `"bypassPermissions"`. The mode labeled **Manual** arrives as `"default"`, never as `"manual"`, so scripts that match `"default"` keep working. Not all events receive this field. Check the JSON example in each [hook event](https://code.claude.com/docs/en/hooks#hook-events) section |
553
- | `effort` | Object with a `level` field holding the active [effort level](https://code.claude.com/docs/en/model-config#adjust-effort-level) for the turn: `"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`. If the requested model effort exceeds what the current model supports, this is the downgraded level the model actually used. Ultracode is not a distinct level and reports as `"xhigh"`. The object matches the [status line](https://code.claude.com/docs/en/statusline#available-data)`effort` field. Present for events that fire within a tool-use context, such as `PreToolUse`, `PostToolUse`, `Stop`, and `SubagentStop`, when the current model supports the effort parameter. The level is also available to hook commands and the Bash tool as the `$CLAUDE_EFFORT` environment variable. |
650
+ | `effort` | Object with a `level` field holding the [effort level](https://code.claude.com/docs/en/model-config#adjust-effort-level) in effect when the hook runs: `"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`. If you set a level the active model doesn’t support, `level` reports the level Claude Code ran instead; [Adjust effort level](https://code.claude.com/docs/en/model-config#adjust-effort-level) says how it picks that level. The object matches the [status line](https://code.claude.com/docs/en/statusline#available-data)`effort` field. Present for events that fire within a tool-use context, such as `PreToolUse`, `PostToolUse`, `Stop`, and `SubagentStop`, when the current model supports the effort parameter. The level is also available to hook commands and the Bash tool as the `$CLAUDE_EFFORT` environment variable. |
554
651
  | `hook_event_name` | Name of the event that fired |
555
652
 
556
- When running with `--agent` or inside a subagent, two additional fields are included:
653
+ `agent_id` and `agent_type` tell your script which agent a hook fired in, such as a subagent, an [in-process teammate](https://code.claude.com/docs/en/agent-teams#choose-a-display-mode), or the agent you chose with `--agent`:
557
654
 
558
655
  | Field | Description |
559
656
  | --- | --- |
560
- | `agent_id` | Unique identifier for the subagent. Present only when the hook fires inside a subagent call. Use this to distinguish subagent hook calls from main-thread calls. |
561
- | `agent_type` | Agent name (for example, `"Explore"` or `"security-reviewer"`). Present when the session uses `--agent` or the hook fires inside a subagent. For subagents, the subagent’s type takes precedence over the session’s `--agent` value. For [custom subagents](https://code.claude.com/docs/en/sub-agents), this is the `name` field from the agent’s frontmatter, not the filename. For subagents shipped by a [plugin](https://code.claude.com/docs/en/plugins), this is the plugin-scoped identifier such as `my-plugin:reviewer`, not the bare frontmatter name. See [SubagentStart](https://code.claude.com/docs/en/hooks#subagentstart) for how to write a matcher against a plugin-scoped name. |
657
+ | `agent_id` | Unique identifier for the subagent or in-process teammate the hook fires in. |
658
+ | `agent_type` | Agent name (for example, `"Explore"` or `"security-reviewer"`). Present when the session uses `--agent` or the hook fires inside a subagent. For subagents, the subagent’s type takes precedence over the session’s `--agent` value. See [SubagentStart](https://code.claude.com/docs/en/hooks#subagentstart) for the values custom and plugin subagents report and how to write a matcher against a plugin-scoped name. |
562
659
 
563
- Only [`SessionStart`](https://code.claude.com/docs/en/hooks#sessionstart) hooks can receive a `model` field, and it is not guaranteed to be present. There is no `$CLAUDE_MODEL` environment variable. A hook process inherits the parent environment, so it can read `$ANTHROPIC_MODEL` if you set it in your shell, but that value doesn’t change when you switch models with `/model` during a session.For example, a `PreToolUse` hook for a Bash command receives this on stdin:
660
+ Only [`SessionStart`](https://code.claude.com/docs/en/hooks#sessionstart) hooks can receive a `model` field, and Claude Code doesn’t always include it. [`PreModelSwitch`](https://code.claude.com/docs/en/hooks#premodelswitch) and [`PostModelSwitch`](https://code.claude.com/docs/en/hooks#postmodelswitch) hooks receive `from_model` and `to_model` instead, so use a PostModelSwitch hook to follow the model as it changes during a session.There is no `$CLAUDE_MODEL` environment variable. The hook can read `$ANTHROPIC_MODEL` if you set it in your shell, but that value doesn’t change when you switch models with `/model` during a session.A hook process inherits the parent environment, apart from the `OTEL_*` exporter variables that Claude Code [removes from every subprocess it spawns](https://code.claude.com/docs/en/monitoring-usage#administrator-configuration) and, when [`CLAUDE_CODE_SUBPROCESS_ENV_SCRUB`](https://code.claude.com/docs/en/env-vars#variables) is set to `1`, the variables it strips. In a session that [gets the HIPAA configuration](https://code.claude.com/docs/en/hipaa-setup#check-how-developers-sign-in-and-connect), Claude Code also [removes Anthropic credentials](https://code.claude.com/docs/en/hipaa-setup#anthropic-credentials-in-commands-hooks-and-mcp-servers) from the hook’s environment.For example, a `PreToolUse` hook for a Bash command receives this on stdin:
564
661
 
565
662
  ```
566
663
  {
@@ -568,25 +665,73 @@ Only [`SessionStart`](https://code.claude.com/docs/en/hooks#sessionstart) hooks
568
665
  "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
569
666
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
570
667
  "cwd": "/home/user/my-project",
668
+ "scratchpad_dir": "/tmp/claude-1000/-home-user-my-project/abc123/scratchpad",
571
669
  "permission_mode": "default",
572
670
  "hook_event_name": "PreToolUse",
573
671
  "tool_name": "Bash",
574
672
  "tool_input": {
575
- "command": "npm test"
576
- }
673
+ "command": "npm test",
674
+ "description": "Run test suite",
675
+ "timeout": 120000,
676
+ "run_in_background": false
677
+ },
678
+ "tool_use_id": "toolu_01ABC123..."
577
679
  }
578
680
  ```
579
681
 
580
- The `tool_name` and `tool_input` fields are event-specific. Each [hook event](https://code.claude.com/docs/en/hooks#hook-events) section documents the additional fields for that event.
682
+ The `tool_name`, `tool_input`, and `tool_use_id` fields are event-specific. Each [hook event](https://code.claude.com/docs/en/hooks#hook-events) section documents the additional fields for that event.
581
683
 
582
684
  ### [​](https://code.claude.com/docs/en/hooks\#exit-code-output) Exit code output
583
685
 
584
- The exit code from your hook command tells Claude Code whether the action should proceed, be blocked, or be ignored.**Exit 0** means success. Claude Code parses stdout for [JSON output fields](https://code.claude.com/docs/en/hooks#json-output). JSON output is only processed on exit 0. For most events, stdout is written to the debug log but not shown in the transcript. The exceptions are `UserPromptSubmit`, `UserPromptExpansion`, and `SessionStart`, where stdout is added as context that Claude can see and act on.**Exit 2** means a blocking error. Claude Code ignores stdout and any JSON in it. Instead, stderr text is fed back to Claude as an error message. The effect depends on the event: `PreToolUse` blocks the tool call, `UserPromptSubmit` rejects the prompt, and so on. See [exit code 2 behavior](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event) for the full list.**Any other exit code** is a non-blocking error for most hook events. The transcript shows a `<hook name> hook error` notice followed by the first line of stderr, so you can identify the cause without `--debug`. Execution continues and the full stderr is written to the debug log.For example, a hook command script that blocks dangerous Bash commands:
686
+ Your hook’s exit code tells Claude Code whether to continue with the action that triggered the hook, such as a tool call or a prompt. A run that finishes has one of three outcomes:
687
+
688
+ - **Success**: your hook exits 0. Claude Code applies any [JSON output](https://code.claude.com/docs/en/hooks#json-output) fields your hook printed, and the action goes ahead unless those fields block or deny it.
689
+ - **Blocking error**: your hook exits 2. On [events that can block](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event), Claude Code stops the action.
690
+ - **Non-blocking error**: your hook exits with any other code, or fails in some other way, such as not starting or printing invalid JSON. The action goes ahead, and on events such as `PreToolUse` you see a `<hook name> hook error` notice in the transcript. If you want a failed hook to block the action, set [`onFailure: "block"`](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails).
691
+
692
+ What your hook prints to stdout can change the outcome. For example, if a `PreToolUse` hook exits 1 but prints JSON that passes validation, the run is a success and the JSON fields decide what happens. To find your hook’s outcome on an event such as `PreToolUse`, match what it printed to stdout in the first column with its exit code along the top:
693
+
694
+ | Stdout | Exit 0 | Exit 2 | Any other exit code |
695
+ | --- | --- | --- | --- |
696
+ | JSON object that passes [schema validation](https://code.claude.com/docs/en/hooks#json-output) | Success. The fields apply | Blocking error. Claude Code still reads the fields, but they can’t override the block | Success. Claude Code ignores the exit code, and the fields alone decide. With [`onFailure: "block"`](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails), this counts as a failure |
697
+ | JSON that [can’t be parsed](https://code.claude.com/docs/en/hooks#exit-code-0) or fails schema validation | Non-blocking error. The notice carries the parse or validation message | Blocking error. Your stderr is the reason | Non-blocking error. The notice carries the parse or validation message |
698
+ | [Plain text](https://code.claude.com/docs/en/hooks#exit-code-0), or nothing | Success | Blocking error. Your stderr is the reason | Non-blocking error. The notice carries the first line of your stderr |
699
+
700
+ Some events have their own rules:
701
+
702
+ - **`WorktreeCreate`**: any non-zero exit code makes worktree creation fail, whatever your JSON says.
703
+ - **`WorktreeRemove`**: any non-zero exit code makes worktree removal fail if the directory still exists afterward.
704
+ - **`Stop`, `SubagentStop`, `TaskCompleted`, and a plugin’s `UserPromptSubmit` hook**: when your hook exits 2 with nothing on stdout and its stderr says a file is missing, such as `No such file or directory`, Claude Code treats the run as a non-blocking error.
705
+ - **`Elicitation` and `ElicitationResult`**: Claude Code applies your `hookSpecificOutput` when your hook exits 0, and ignores it on any other exit code.
706
+ - **Events that discard hook output, such as `StopFailure`**: Claude Code ignores your JSON on any exit code, apart from side-effect fields like `terminalSequence`, which still fire.
707
+
708
+ To check what exit code 2 does on your event, see [Exit code 2 behavior per event](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event). To check which decision fields it honors, see [Decision control](https://code.claude.com/docs/en/hooks#decision-control).
709
+
710
+ #### [​](https://code.claude.com/docs/en/hooks\#exit-code-0) Exit code 0
711
+
712
+ Exit 0 means success, and is the intended exit code when you print JSON for structured control.For most events, Claude Code writes stdout to the debug log and doesn’t show it in the transcript. The exceptions are `UserPromptSubmit`, `UserPromptExpansion`, `SessionStart`, and `PostModelSwitch`, where Claude Code adds plain-text stdout as context that Claude can see and act on.For a hook that isn’t [async](https://code.claude.com/docs/en/hooks#how-async-hooks-execute), Claude Code parses your stdout as [JSON output](https://code.claude.com/docs/en/hooks#json-output) when the whole output is one JSON object with nothing around it but whitespace, and otherwise as plain text or a parse failure:
713
+
714
+ - **One JSON object, on one line or several**: parsed as JSON output.
715
+ - **Output that doesn’t start with `{`, or that starts with `{` and doesn’t end with `}`**: plain text. A JSON array and a quoted JSON string are plain text by this rule.
716
+ - **Two or more lines that each parse as JSON on their own, the first starting with `{` and the last ending with `}`**: plain text when no line is a JSON output object that sets a field, and a parse failure when one is.
717
+ - **Anything else that starts with `{` and ends with `}` but isn’t valid JSON**: a parse failure.
718
+
719
+ When Claude Code tries to parse your stdout as JSON and can’t, or the parsed object fails [schema validation](https://code.claude.com/docs/en/hooks#json-output), the run is a [non-blocking error](https://code.claude.com/docs/en/hooks#exit-code-output). The `<hook name> hook error` notice carries the parse or validation message. On the events that add plain-text stdout as context, Claude Code doesn’t add stdout it failed to parse.Claude never sees stderr from a hook that exits 0. To read it yourself on events such as `PreToolUse`, enable [debug logging](https://code.claude.com/docs/en/hooks#debug-hooks). To surface a warning to Claude from a `PostToolUse` or `PostToolUseFailure` hook, exit 2 instead so [Claude sees the stderr](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event) even though the tool already ran.
720
+
721
+ #### [​](https://code.claude.com/docs/en/hooks\#exit-code-2) Exit code 2
722
+
723
+ Exit with code 2 to block the action. On [events that can block](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event), Claude Code stops the action: a `PreToolUse` hook blocks the tool call, for example, and a `UserPromptSubmit` hook rejects the prompt.The message that comes with the block is your hook’s stderr. If your hook also printed JSON that makes a blocking decision, Claude Code uses that decision’s reason instead.Exit 2 blocks even when your hook prints JSON:
724
+
725
+ - **JSON that passes schema validation**: Claude Code still reads the [JSON output](https://code.claude.com/docs/en/hooks#json-output) fields, but they can’t override the block. Even a `permissionDecision` of `"allow"` doesn’t let the action through. On `Elicitation` and `ElicitationResult`, an exit-2 hook’s `hookSpecificOutput` is ignored.
726
+ - **JSON that fails schema validation**: the hook still blocks. Claude Code uses your stderr as the blocking reason and records the validation failure in the debug log.
727
+
728
+ This script blocks `rm` commands by exiting 2 and leaves every other command to the normal permission flow:
585
729
 
586
730
  ```
587
731
  #!/bin/bash
588
732
  # Reads JSON input from stdin, checks the command
589
- command=$(jq -r '.tool_input.command' < /dev/stdin)
733
+ input=$(cat)
734
+ command=$(jq -r '.tool_input.command' <<<"$input")
590
735
 
591
736
  if [[ "$command" == rm* ]]; then
592
737
  echo "Blocked: rm commands are not allowed" >&2
@@ -596,7 +741,74 @@ fi
596
741
  exit 0 # No decision: the normal permission flow applies
597
742
  ```
598
743
 
599
- For most hook events, only exit code 2 blocks the action. Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use `exit 2`. The exception is `WorktreeCreate`, where any non-zero exit code aborts worktree creation.
744
+ With this script registered as a `PreToolUse` hook on `Bash`, a command that starts with `rm` is blocked, and Claude receives the hook’s stderr as the tool’s error, prefixed with the event name, the tool name, and the hook’s command:
745
+
746
+ ```
747
+ PreToolUse:Bash hook error: [${CLAUDE_PROJECT_DIR}/.claude/hooks/no-rm.sh]: Blocked: rm commands are not allowed
748
+ ```
749
+
750
+ #### [​](https://code.claude.com/docs/en/hooks\#other-exit-codes) Other exit codes
751
+
752
+ When your hook exits with a code other than 0 or 2 and prints plain text or nothing to stdout, the run is a [non-blocking error](https://code.claude.com/docs/en/hooks#exit-code-output). You see a `<hook name> hook error` notice in the transcript with `Failed with non-blocking status code:` and the first line of your hook’s stderr. For example, when a `PreToolUse` hook on `Bash` prints `something broke` to stderr and exits 1, the `PreToolUse:Bash hook error` notice carries this line:
753
+
754
+ ```
755
+ Failed with non-blocking status code: something broke
756
+ ```
757
+
758
+ To capture the full stderr rather than its first line, enable [debug logging](https://code.claude.com/docs/en/hooks#debug-hooks).A hook that can’t start is a non-blocking error too. In shell form, when the script path doesn’t exist or isn’t executable, the shell exits with a code like 127 and the notice carries the interpreter’s message, for example `Failed with non-blocking status code: /bin/sh: /path/to/hook.sh: No such file or directory`. When 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 instead, set [`onFailure: "block"`](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails).
759
+
760
+ Without valid JSON on stdout, Claude Code treats exit code 1 as a non-blocking error, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use `exit 2`.
761
+
762
+ #### [​](https://code.claude.com/docs/en/hooks\#timeouts) Timeouts
763
+
764
+ Apart from a command hook you run with [`async: true`](https://code.claude.com/docs/en/hooks#run-hooks-in-the-background), Claude Code cancels a `command`, `http`, or `mcp_tool` hook that reaches its [`timeout`](https://code.claude.com/docs/en/hooks#common-fields), discarding the hook’s output, so on most events a timed-out hook renders no decision.On [`PreModelSwitch`](https://code.claude.com/docs/en/hooks#premodelswitch), a hook canceled at its timeout blocks the model switch. On `PreToolUse`, the two hook families differ:
765
+
766
+ - A timed-out `command`, `http`, or `mcp_tool` hook doesn’t block the tool call. The call continues through the normal [permission flow](https://code.claude.com/docs/en/permissions), so don’t count on a stalled hook to act as a gate. To block the call when a `command` or `http` hook times out, set [`onFailure: "block"`](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails).
767
+ - An [Agent SDK callback hook](https://code.claude.com/docs/en/agent-sdk/hooks) that exceeds its timeout [blocks the tool call](https://code.claude.com/docs/en/hooks#pretooluse).
768
+
769
+ #### [​](https://code.claude.com/docs/en/hooks\#block-the-action-when-a-hook-fails) Block the action when a hook fails
770
+
771
+ On most events, when a hook fails or times out, Claude Code still carries out the action, so a policy hook with a wrong path or a crashing script lets everything through. To block the action instead, set `"onFailure": "block"` on a `command` or `http` hook. The default value is `"continue"`. Requires Claude Code v2.1.295 or later.This `PreToolUse` hook in `.claude/settings.json` runs a project script before each Bash command, and blocks the command if the script fails:
772
+
773
+ ```
774
+ {
775
+ "hooks": {
776
+ "PreToolUse": [\
777
+ {\
778
+ "matcher": "Bash",\
779
+ "hooks": [\
780
+ {\
781
+ "type": "command",\
782
+ "command": "node",\
783
+ "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],\
784
+ "onFailure": "block"\
785
+ }\
786
+ ]\
787
+ }\
788
+ ]
789
+ }
790
+ }
791
+ ```
792
+
793
+ To test it, leave `check-command.js` missing and ask Claude to run a Bash command such as `ls`. Claude Code blocks the call, and the error includes `failed; blocking because onFailure is "block"` followed by node’s own error output, trimmed here to one line:
794
+
795
+ ```
796
+ PreToolUse:Bash hook error: [node ${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js]: failed; blocking because onFailure is "block"
797
+ Error: Cannot find module '/path/to/project/.claude/hooks/check-command.js'
798
+ ```
799
+
800
+ After a timeout, the message says `timed out` instead of `failed`. Without `onFailure` set, the same missing script is a non-blocking error and `ls` runs.Each of these counts as a failure:
801
+
802
+ - **Can’t start**: a command hook fails to start, for example because the script or executable doesn’t exist
803
+ - **Exit code other than 0 or 2**: counts for a command hook even if it printed JSON that allows the action, such as `permissionDecision: "allow"`. To return a JSON decision, exit 0
804
+ - **HTTP error**: an HTTP hook’s connection fails, or the response status isn’t 2xx
805
+ - **Timeout**: the hook reaches its [`timeout`](https://code.claude.com/docs/en/hooks#common-fields)
806
+ - **Invalid output**: the JSON output [can’t be parsed](https://code.claude.com/docs/en/hooks#exit-code-0) or fails [schema validation](https://code.claude.com/docs/en/hooks#json-output). For an HTTP hook, a 2xx body that is neither empty nor a JSON object also counts. Plain-text stdout from a command hook isn’t a failure
807
+
808
+ With `"block"` set, a failure does what [exit code 2 does on that event](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event), except on `PermissionRequest`, where it denies the request. For example, a `PreToolUse` failure blocks the tool call and a `UserPromptSubmit` failure blocks the prompt.The field has no effect on these hooks:
809
+
810
+ - **`Stop`, `SubagentStop`, `TaskCompleted`, and `TeammateIdle` hooks**: exit code 2 on these events sends Claude back to keep working, and Claude can’t repair a hook that won’t run
811
+ - **Background command hooks**: command hooks that set [`async` or `asyncRewake`](https://code.claude.com/docs/en/hooks#run-hooks-in-the-background)
600
812
 
601
813
  #### [​](https://code.claude.com/docs/en/hooks\#exit-code-2-behavior-per-event) Exit code 2 behavior per event
602
814
 
@@ -605,8 +817,8 @@ Exit code 2 is the way a hook signals “stop, don’t do this.” The effect de
605
817
  | Hook event | Can block? | What happens on exit 2 |
606
818
  | --- | --- | --- |
607
819
  | `PreToolUse` | Yes | Blocks the tool call |
608
- | `PermissionRequest` | Yes | Denies the permission |
609
- | `UserPromptSubmit` | Yes | Blocks prompt processing and erases the prompt |
820
+ | `PermissionRequest` | No | Exit code 2 isn’t honored for this event and the permission flow proceeds unchanged. Deny through the [`decision` object](https://code.claude.com/docs/en/hooks#permissionrequest-decision-control) instead |
821
+ | `UserPromptSubmit` | Yes | Blocks the prompt, so it never reaches Claude. See [What a blocked prompt leaves behind](https://code.claude.com/docs/en/hooks#what-a-blocked-prompt-leaves-behind) |
610
822
  | `UserPromptExpansion` | Yes | Blocks the expansion |
611
823
  | `Stop` | Yes | Prevents Claude from stopping, continues the conversation |
612
824
  | `SubagentStop` | Yes | Prevents the subagent from stopping |
@@ -614,70 +826,82 @@ Exit code 2 is the way a hook signals “stop, don’t do this.” The effect de
614
826
  | `TaskCreated` | Yes | Rolls back the task creation |
615
827
  | `TaskCompleted` | Yes | Prevents the task from being marked as completed |
616
828
  | `ConfigChange` | Yes | Blocks the configuration change from taking effect (except `policy_settings`) |
617
- | `StopFailure` | No | Output and exit code are ignored |
829
+ | `StopFailure` | No | Output and exit code are ignored, except `terminalSequence` |
618
830
  | `PostToolUse` | No | Shows stderr to Claude; the tool already ran |
619
831
  | `PostToolUseFailure` | No | Shows stderr to Claude; the tool already failed |
620
832
  | `PostToolBatch` | Yes | Stops the agentic loop before the next model call |
621
- | `PermissionDenied` | No | Exit code and stderr are ignored because the denial already occurred. Use JSON `hookSpecificOutput.retry: true` to tell the model it may retry |
622
- | `Notification` | No | Shows stderr to user only |
833
+ | `PermissionDenied` | No | Exit code and stderr are ignored because the denial already occurred. Use JSON `hookSpecificOutput.retry: true` to tell the model it may retry; Claude Code ignores `retry: true` for [no-verdict denials](https://code.claude.com/docs/en/hooks#permissiondenied-decision-control) |
834
+ | `Notification` | No | Exit code and stderr are ignored |
623
835
  | `SubagentStart` | No | Shows stderr to user only |
624
836
  | `SessionStart` | No | Shows stderr to user only |
625
- | `Setup` | No | Shows stderr to user only |
837
+ | `Setup` | No | Exit code and stderr are ignored |
626
838
  | `SessionEnd` | No | Shows stderr to user only |
627
839
  | `CwdChanged` | No | Shows stderr to user only |
840
+ | `DirectoryAdded` | No | Stderr goes to the debug log; the directory is already added |
628
841
  | `FileChanged` | No | Shows stderr to user only |
629
842
  | `PreCompact` | Yes | Blocks compaction |
630
843
  | `PostCompact` | No | Shows stderr to user only |
631
- | `Elicitation` | Yes | Denies the elicitation |
844
+ | `PreModelSwitch` | Yes | Blocks the model switch and shows stderr to the user |
845
+ | `PostModelSwitch` | No | Shows stderr to user only; the model already switched |
846
+ | `Elicitation` | Yes | Declines the request, and no dialog appears |
632
847
  | `ElicitationResult` | Yes | Blocks the response (action becomes decline) |
633
848
  | `WorktreeCreate` | Yes | Any non-zero exit code causes worktree creation to fail |
634
- | `WorktreeRemove` | No | Failures are logged in debug mode only |
849
+ | `WorktreeRemove` | Yes | Any non-zero exit code causes worktree removal to fail if the directory still exists afterward. See [WorktreeRemove](https://code.claude.com/docs/en/hooks#worktreeremove) for what happens to the directory |
635
850
  | `InstructionsLoaded` | No | Exit code is ignored |
636
851
  | `MessageDisplay` | No | The original text is displayed |
637
852
 
638
- For `SessionStart`, `Setup`, and `SubagentStart`, the exit code 2 stderr renders in the transcript as a `<hook name> hook error` notice, the same way a [non-blocking error](https://code.claude.com/docs/en/hooks#exit-code-output) does. Claude doesn’t see it, and the session or subagent proceeds. For `SubagentStart`, the notice appears in the subagent’s own transcript, not in the parent conversation.As of Claude Code v2.1.199, `SessionStart`, `Setup`, and `SubagentStart` show exit code 2 stderr in the transcript. Earlier versions wrote it to the debug log only.
853
+ For `SessionStart`, `SubagentStart`, and `PostModelSwitch`, Claude Code renders the exit code 2 stderr in the transcript as a `<hook name> hook error` notice, the same way it renders a [non-blocking error](https://code.claude.com/docs/en/hooks#exit-code-output). Claude doesn’t see it, and the session or subagent proceeds. For `SubagentStart`, the notice appears in the subagent’s own transcript, not in the parent conversation.
639
854
 
640
855
  ### [​](https://code.claude.com/docs/en/hooks\#http-response-handling) HTTP response handling
641
856
 
642
- HTTP hooks use HTTP status codes and response bodies instead of exit codes and stdout:
857
+ HTTP hooks use HTTP status codes and response bodies instead of exit codes and stdout. The outcomes below apply to most events; an event with its own failure contract in the [per-event table](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event), such as `WorktreeCreate`, applies that contract to a failed HTTP hook too:
643
858
 
644
859
  - **2xx with an empty body**: success, equivalent to exit code 0 with no output
645
- - **2xx with a plain text body**: success, the text is added as context
646
- - **2xx with a JSON body**: success, parsed using the same [JSON output](https://code.claude.com/docs/en/hooks#json-output) schema as command hooks
860
+ - **2xx with a JSON object body**: parsed using the same [JSON output](https://code.claude.com/docs/en/hooks#json-output) schema as command hooks. A body that fails schema validation is a non-blocking error
861
+ - **2xx with any other body, such as plain text**: non-blocking error, handled the same as a non-2xx status. Claude Code doesn’t add the text to Claude’s context
647
862
  - **Non-2xx status**: non-blocking error, execution continues
648
- - **Connection failure or timeout**: non-blocking error, execution continues
863
+ - **Connection failure**: non-blocking error, execution continues
864
+ - **Timeout**: the hook is canceled, as described under [Timeouts](https://code.claude.com/docs/en/hooks#timeouts)
649
865
 
650
- Unlike command hooks, HTTP hooks can’t signal a blocking error through status codes alone. To block a tool call or deny a permission, return a 2xx response with a JSON body containing the appropriate decision fields.
866
+ HTTP hooks can’t signal a blocking error through the status code alone: a non-2xx status or a failed connection is a [non-blocking error](https://code.claude.com/docs/en/hooks#exit-code-output). To block a tool call or deny a permission, return a 2xx response with a JSON body containing the appropriate decision fields. To block the action when the request fails or returns a non-2xx status, set [`onFailure: "block"`](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails).
651
867
 
652
868
  ### [​](https://code.claude.com/docs/en/hooks\#json-output) JSON output
653
869
 
654
870
  Exit 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](https://code.claude.com/docs/en/hooks#decision-control) for blocking, allowing, or escalating to the user.
655
871
 
656
- You must choose one approach per hook, not both: either use exit codes alone for signaling, or exit 0 and print JSON for structured control. Claude Code only processes JSON on exit 0. If you exit 2, any JSON is ignored.
872
+ 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](https://code.claude.com/docs/en/hooks#exit-code-2-behavior-per-event), and Claude Code still reads the JSON fields, with the one elicitation exception noted under [Exit code 2](https://code.claude.com/docs/en/hooks#exit-code-2).
873
+
874
+ Print nothing but the JSON object to stdout. For a hook that isn’t [async](https://code.claude.com/docs/en/hooks#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](https://code.claude.com/docs/en/hooks-guide#hook-json-has-no-effect) shows how to find and silence that text.A hook’s `additionalContext`, `systemMessage`, and `initialUserMessage` strings, and its plain stdout, are capped at 10,000 characters:
657
875
 
658
- Your hook’s stdout must contain only the JSON object. If your shell profile prints text on startup, it can interfere with JSON parsing. See [JSON validation failed](https://code.claude.com/docs/en/hooks-guide#json-validation-failed) in the troubleshooting guide.Hook output strings, including `additionalContext`, `systemMessage`, and plain stdout, are capped at 10,000 characters. Output that exceeds this limit is saved to a file and replaced with a preview and file path, the same way large tool results are handled.The JSON object supports three kinds of fields:
876
+ - **Scope**: Claude Code measures each string on its own, even when several hooks run for the same event. For JSON output, each field is measured separately; plain stdout is measured whole.
877
+ - **Over the limit**: Claude Code saves the output to a file in the session directory and replaces it with the file path and a preview of up to the first 2,000 characters. A large valid Bash result is handled the same way, described under [Output limits](https://code.claude.com/docs/en/tools-reference#output-limits). Unlike that Bash ceiling, this cap has no setting or environment variable to raise it.
878
+ - **Reading the file**: Claude Code doesn’t ask Claude to read the file, so keep anything Claude must always see within the cap.
659
879
 
660
- - **Universal fields** like `continue` work across all events. These are listed in the table below.
880
+ The JSON object supports three kinds of fields:
881
+
882
+ - **Universal fields** like `continue` are listed in the table below. Every event accepts them, but some events discard them or deliver `systemMessage` somewhere other than the transcript. Each event’s section says so. `terminalSequence` works on those events too, with the exceptions listed under [Emit terminal notifications](https://code.claude.com/docs/en/hooks#emit-terminal-notifications).
661
883
  - **Top-level `decision` and `reason`** are used by some events to block or provide feedback.
662
884
  - **`hookSpecificOutput`** is a nested object for events that need richer control. It requires a `hookEventName` field set to the event name.
663
885
 
664
886
  | Field | Default | Description |
665
887
  | --- | --- | --- |
666
888
  | `continue` | `true` | If `false`, Claude stops processing entirely after the hook runs. Takes precedence over any event-specific decision fields |
667
- | `stopReason` | none | Message shown to the user when `continue` is `false`. Not shown to Claude |
668
- | `suppressOutput` | `false` | If `true`, hides the hook’s stdout from the transcript. Stdout still appears in the debug log |
669
- | `systemMessage` | none | Warning message shown to the user |
889
+ | `stopReason` | none | Message shown to the user when `continue` is `false`. It stays in the conversation, so Claude sees it if the conversation continues |
890
+ | `suppressOutput` | `false` | Has no effect: Claude Code accepts the field but doesn’t act on it. A successful hook’s stdout is never shown in the transcript and is recorded in the debug log |
891
+ | `systemMessage` | none | Warning message shown to the user. In [Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview) and [`--output-format stream-json`](https://code.claude.com/docs/en/headless) output, it can arrive as an [`SDKInformationalMessage`](https://code.claude.com/docs/en/agent-sdk/typescript#sdkinformationalmessage) |
670
892
  | `terminalSequence` | none | A terminal escape sequence for Claude Code to emit on your behalf, such as a desktop notification, window title, or bell. Restricted to OSC `0`/`1`/`2`/`9`/`99`/`777` and BEL. If the value contains anything outside the allowlist, the field is ignored. Use this instead of writing to `/dev/tty`, which is unavailable to hooks |
671
893
 
672
- To stop Claude entirely regardless of event type:
894
+ To stop Claude entirely:
673
895
 
674
896
  ```
675
897
  { "continue": false, "stopReason": "Build failed, fix errors before continuing" }
676
898
  ```
677
899
 
900
+ For `PreToolUse` and `PostToolUse` hooks, the stop applies even when the tool call fails or completes while Claude is still streaming a response.
901
+
678
902
  #### [​](https://code.claude.com/docs/en/hooks\#emit-terminal-notifications) Emit terminal notifications
679
903
 
680
- The `terminalSequence` field requires Claude Code v2.1.141 or later.Hooks run without a controlling terminal, so writing escape sequences directly to `/dev/tty` fails. Instead, return the escape sequence in the `terminalSequence` field and Claude Code emits it for you through its own terminal write path. This is race-free, works inside tmux and GNU screen, and works on Windows where there is no `/dev/tty`.The field accepts a string of one or more allowlisted escape sequences:
904
+ Hooks run without a controlling terminal, so writing escape sequences directly to `/dev/tty` fails. Instead, return the escape sequence in the `terminalSequence` field and Claude Code emits it for you through its own terminal write path. This is race-free, works inside tmux and GNU screen, and works on Windows where there is no `/dev/tty`.The field accepts a string of one or more allowlisted escape sequences:
681
905
 
682
906
  - OSC `0`, `1`, `2`: window and icon titles
683
907
  - OSC `9`: iTerm2, ConEmu, Windows Terminal, and WezTerm notifications, including `9;4` taskbar progress
@@ -685,7 +909,12 @@ The `terminalSequence` field requires Claude Code v2.1.141 or later.Hooks run wi
685
909
  - OSC `777`: urxvt, Ghostty, and Warp notifications
686
910
  - Bare BEL
687
911
 
688
- Sequences may be terminated with BEL or with ST. Anything outside the allowlist, including CSI cursor and color sequences, OSC palette sequences, OSC 8 hyperlinks, OSC 52 clipboard writes, and OSC 1337, is rejected and the field is ignored.The example below fires a desktop notification from a `Notification` hook. The escape sequence is built with `printf` octal escapes so the control bytes never appear on the shell command line, and `jq -n --arg` builds the JSON output so quotes, backslashes, and newlines in the notification message are escaped correctly:
912
+ Sequences may be terminated with BEL or with ST. Anything outside the allowlist, including CSI cursor and color sequences, OSC palette sequences, OSC 8 hyperlinks, OSC 52 clipboard writes, and OSC 1337, is rejected and the field is ignored.Claude Code writes the sequence itself when it processes your hook’s output, so the field works on events that discard `systemMessage` and `continue`, such as `Notification` and `StopFailure`. It has two limits:
913
+
914
+ - Claude Code writes the sequence only in an interactive session, and only while its interface is on screen. In non-interactive mode with the `-p` flag and in the Agent SDK, it ignores the field.
915
+ - A `WorktreeCreate` command hook can’t return JSON, because Claude Code reads its stdout as the worktree path. An HTTP `WorktreeCreate` hook returns JSON and can include the field.
916
+
917
+ The example below fires a desktop notification from a `Notification` hook. The escape sequence is built with `printf` octal escapes so the control bytes never appear on the shell command line, and `jq -n --arg` builds the JSON output so quotes, backslashes, and newlines in the notification message are escaped correctly:
689
918
 
690
919
  ```
691
920
  #!/bin/bash
@@ -697,13 +926,11 @@ seq=$(printf '\033]777;notify;%s;%s\007' "$title" "$body")
697
926
  jq -nc --arg seq "$seq" '{terminalSequence: $seq}'
698
927
  ```
699
928
 
700
- The `{ "terminalSequence": "..." }` shape is the same from any shell or language. On Windows, build the escape string in PowerShell or a script and emit the same JSON object.
701
-
702
- `terminalSequence` is the supported replacement for hooks that previously wrote escape sequences directly to `/dev/tty`. The allowlist is restricted to sequences that can’t move the cursor or alter colors, so a hook can never corrupt an on-screen prompt.
929
+ The `{ "terminalSequence": "..." }` shape is the same from any shell or language.
703
930
 
704
931
  #### [​](https://code.claude.com/docs/en/hooks\#add-context-for-claude) Add context for Claude
705
932
 
706
- The `additionalContext` field passes a string from your hook into Claude’s context window. Claude Code wraps the string in a system reminder and inserts it into the conversation at the point where the hook fired. Claude reads the reminder on the next model request, but it doesn’t appear as a chat message in the interface.Return `additionalContext` inside `hookSpecificOutput` alongside the event name:
933
+ The `additionalContext` field passes a string from your hook into Claude’s context window. Claude Code wraps the string in a [system reminder](https://code.claude.com/docs/en/glossary#system-reminder) and inserts it into the conversation at the point where the hook fired. Claude reads the reminder on the next model request, but it doesn’t appear as a chat message in the interface.Return `additionalContext` inside `hookSpecificOutput` alongside the event name:
707
934
 
708
935
  ```
709
936
  {
@@ -716,18 +943,19 @@ The `additionalContext` field passes a string from your hook into Claude’s con
716
943
 
717
944
  Where the reminder appears depends on the event:
718
945
 
719
- - [SessionStart](https://code.claude.com/docs/en/hooks#sessionstart), [Setup](https://code.claude.com/docs/en/hooks#setup), and [SubagentStart](https://code.claude.com/docs/en/hooks#subagentstart): at the start of the conversation, before the first prompt
946
+ - [SessionStart](https://code.claude.com/docs/en/hooks#sessionstart) and [SubagentStart](https://code.claude.com/docs/en/hooks#subagentstart): at the start of the conversation, before the first prompt
720
947
  - [UserPromptSubmit](https://code.claude.com/docs/en/hooks#userpromptsubmit) and [UserPromptExpansion](https://code.claude.com/docs/en/hooks#userpromptexpansion): alongside the submitted prompt
721
948
  - [PreToolUse](https://code.claude.com/docs/en/hooks#pretooluse), [PostToolUse](https://code.claude.com/docs/en/hooks#posttooluse), [PostToolUseFailure](https://code.claude.com/docs/en/hooks#posttoolusefailure), and [PostToolBatch](https://code.claude.com/docs/en/hooks#posttoolbatch): next to the tool result
722
949
  - [Stop](https://code.claude.com/docs/en/hooks#stop) and [SubagentStop](https://code.claude.com/docs/en/hooks#subagentstop): at the end of the turn. The conversation continues so Claude can act on the feedback. See [Stop decision control](https://code.claude.com/docs/en/hooks#stop-decision-control)
950
+ - [PostModelSwitch](https://code.claude.com/docs/en/hooks#postmodelswitch): with the next request after the switch. See [PostModelSwitch decision control](https://code.claude.com/docs/en/hooks#postmodelswitch-decision-control) for timing
723
951
 
724
- When several hooks return `additionalContext` for the same event, Claude receives all of the values. If a value exceeds 10,000 characters, Claude Code writes the full text to a file in the session directory and passes Claude the file path with a short preview instead.Use `additionalContext` for information Claude should know about the current state of your environment or the operation that just ran:
952
+ When several hooks return `additionalContext` for the same event, Claude receives all of the values.If your string contains a `<system-reminder>` or `</system-reminder>` tag, Claude receives the string with that tag’s `<` replaced by `&lt;`.If a value exceeds 10,000 characters, Claude Code writes the text to a file in the session directory and passes Claude the file path with a preview of up to the first 2,000 characters instead. Claude can read the file, but Claude Code doesn’t ask it to.Use `additionalContext` for information Claude should know about the current state of your environment or the operation that just ran:
725
953
 
726
954
  - **Environment state**: the current branch, deployment target, or active feature flags
727
955
  - **Conditional project rules**: which test command applies to the file just edited, which directories are read-only in this worktree
728
956
  - **External data**: open issues assigned to you, recent CI results, content fetched from an internal service
729
957
 
730
- For instructions that never change, prefer [CLAUDE.md](https://code.claude.com/docs/en/memory). It loads without running a script and is the standard place for static project conventions.Write the text as factual statements rather than imperative system instructions. Phrasing such as “The deployment target is production” or “This repo uses `bun test`” reads as project information. Text framed as out-of-band system commands can trigger Claude’s prompt-injection defenses, which causes Claude to surface the text to you instead of treating it as context.Once injected, the text is saved in the session transcript. For mid-session events like `PostToolUse` or `UserPromptSubmit`, resuming with `--continue` or `--resume` replays the saved text rather than re-running the hook for past turns, so values like timestamps or commit SHAs become stale on resume. `SessionStart` hooks run again on resume with `source` set to `"resume"`, so they can refresh their context.
958
+ For instructions that never change, prefer [CLAUDE.md](https://code.claude.com/docs/en/memory). It loads without running a script and is the standard place for static project conventions.Write the text as factual statements rather than imperative system instructions. Phrasing such as “The deployment target is production” or “This repo uses `bun test`” reads as project information. Text framed as out-of-band system commands can trigger Claude’s prompt-injection defenses, which causes Claude to surface the text to you instead of treating it as context.Claude Code saves the injected text in the session transcript. For mid-session events like `PostToolUse` or `UserPromptSubmit`, when you resume with `--continue` or `--resume`, Claude Code replays the saved text rather than re-running the hook for past turns, so values like timestamps or commit SHAs become stale. `SessionStart` hooks run again on resume with `source` set to `"resume"`, or `"fork"` if you added `--fork-session`, so they can refresh their context.
731
959
 
732
960
  #### [​](https://code.claude.com/docs/en/hooks\#decision-control) Decision control
733
961
 
@@ -736,16 +964,18 @@ Not every event supports blocking or controlling behavior through JSON. The even
736
964
  | Events | Decision pattern | Key fields |
737
965
  | --- | --- | --- |
738
966
  | UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompact | Top-level `decision` | `decision: "block"`, `reason`. Stop and SubagentStop also accept `hookSpecificOutput.additionalContext` for [non-error feedback that continues the conversation](https://code.claude.com/docs/en/hooks#stop-decision-control) |
739
- | TeammateIdle, TaskCreated, TaskCompleted | Exit code or `continue: false` | Exit code 2 blocks the action with stderr feedback. JSON `{"continue": false, "stopReason": "..."}` also stops the teammate entirely, matching `Stop` hook behavior |
967
+ | TeammateIdle, TaskCompleted | Exit code or `continue: false` | Exit code 2 blocks the action with stderr feedback. JSON `{"continue": false, "stopReason": "..."}` also stops the teammate entirely, matching `Stop` hook behavior; [TaskCompleted ignores it when the `TaskUpdate` tool triggered the event](https://code.claude.com/docs/en/hooks#taskcompleted-decision-control) |
968
+ | TaskCreated | Exit code or top-level `decision` | Exit code 2 or `decision: "block"` [cancels the task](https://code.claude.com/docs/en/hooks#taskcreated-decision-control) and returns the message to Claude. `continue: false` is ignored |
740
969
  | PreToolUse | `hookSpecificOutput` | `permissionDecision` (allow/deny/ask/defer), `permissionDecisionReason` |
970
+ | PreModelSwitch | `hookSpecificOutput` or top-level `decision` | `permissionDecision` (allow/deny/ask), `permissionDecisionReason`. `decision: "block"` also [cancels the switch](https://code.claude.com/docs/en/hooks#premodelswitch-decision-control) |
741
971
  | PermissionRequest | `hookSpecificOutput` | `decision.behavior` (allow/deny) |
742
- | PermissionDenied | `hookSpecificOutput` | `retry: true` tells the model it may retry the denied tool call |
972
+ | PermissionDenied | `hookSpecificOutput` | `retry: true` tells the model it may retry the denied tool call; Claude Code ignores it for [no-verdict denials](https://code.claude.com/docs/en/hooks#permissiondenied-decision-control) |
743
973
  | WorktreeCreate | path return | Command hook prints path on stdout; HTTP hook returns `hookSpecificOutput.worktreePath`. Hook failure or missing path fails creation |
744
- | Elicitation | `hookSpecificOutput` | `action` (accept/decline/cancel), `content` (form field values for accept) |
745
- | ElicitationResult | `hookSpecificOutput` | `action` (accept/decline/cancel), `content` (form field values override) |
974
+ | WorktreeRemove | Exit code | Any non-zero exit code makes the removal fail if the directory still exists afterward. JSON output is discarded |
975
+ | Elicitation, ElicitationResult | `hookSpecificOutput` or top-level `decision` | `action` (accept/decline/cancel), `content` (form field values). `decision: "block"` also [declines](https://code.claude.com/docs/en/hooks#other-ways-to-decline-an-elicitation) |
746
976
  | MessageDisplay | `hookSpecificOutput` | `displayContent` replaces the displayed text on screen. Display-only: the transcript and what Claude sees keep the original |
747
- | SessionStart, Setup, SubagentStart | Context only | `hookSpecificOutput.additionalContext` adds context for Claude. SessionStart also accepts [`initialUserMessage`, `watchPaths`, `sessionTitle`, and `reloadSkills`](https://code.claude.com/docs/en/hooks#sessionstart-decision-control). No blocking or decision control |
748
- | WorktreeRemove, Notification, SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, FileChanged | None | No decision control. Used for side effects like logging or cleanup |
977
+ | SessionStart, SubagentStart, PostModelSwitch | Context only | `hookSpecificOutput.additionalContext` adds context for Claude. SessionStart also accepts [`initialUserMessage`, `watchPaths`, `sessionTitle`, and `reloadSkills`](https://code.claude.com/docs/en/hooks#sessionstart-decision-control). No blocking or decision control |
978
+ | Setup, Notification, SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, DirectoryAdded, FileChanged | None | No decision control. Used for side effects like logging or cleanup |
749
979
 
750
980
  A few events can also rewrite content rather than only allow or block it:
751
981
 
@@ -763,7 +993,7 @@ For redaction or transformation use cases, intercept at `PreToolUse` for outboun
763
993
  - PermissionRequest
764
994
 
765
995
 
766
- Used by `UserPromptSubmit`, `UserPromptExpansion`, `PostToolUse`, `PostToolUseFailure`, `PostToolBatch`, `Stop`, `SubagentStop`, `ConfigChange`, and `PreCompact`. The only value is `"block"`. To allow the action to proceed, omit `decision` from your JSON, or exit 0 without any JSON at all:
996
+ 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:
767
997
 
768
998
  ```
769
999
  {
@@ -808,7 +1038,7 @@ Each event corresponds to a point in Claude Code’s lifecycle where hooks can r
808
1038
 
809
1039
  ### [​](https://code.claude.com/docs/en/hooks\#sessionstart) SessionStart
810
1040
 
811
- Runs when Claude Code starts a new session or resumes an existing session. Useful for loading development context like existing issues or recent changes to your codebase, or setting up environment variables. For static context that doesn’t require a script, use [CLAUDE.md](https://code.claude.com/docs/en/memory) instead.SessionStart runs on every session, so keep these hooks fast. Only `type: "command"` and `type: "mcp_tool"` hooks are supported.The matcher value corresponds to how the session was initiated:
1041
+ Runs when Claude Code starts a new session or resumes an existing session. Useful for loading development context like existing issues or recent changes to your codebase, or setting up environment variables. For static context that doesn’t require a script, use [CLAUDE.md](https://code.claude.com/docs/en/memory) instead.SessionStart runs on every session, so keep these hooks fast. Only `type: "command"` and `type: "mcp_tool"` hooks are supported. See [MCP tool hook fields](https://code.claude.com/docs/en/hooks#mcp-tool-hook-fields) for when `mcp_tool` hooks run.The matcher value corresponds to how the session was initiated:
812
1042
 
813
1043
  | Matcher | When it fires |
814
1044
  | --- | --- |
@@ -816,6 +1046,9 @@ Runs when Claude Code starts a new session or resumes an existing session. Usefu
816
1046
  | `resume` | `--resume`, `--continue`, or `/resume` |
817
1047
  | `clear` | `/clear` |
818
1048
  | `compact` | Auto or manual compaction |
1049
+ | `fork` | A new session forked from an existing one: `--fork-session` with `--resume` or `--continue`, the `/fork` background copy, `/branch`, or a conversation you [move to the background](https://code.claude.com/docs/en/agent-view#from-inside-a-session) |
1050
+
1051
+ Before v2.1.214, forked sessions reported source `"resume"`.When you start an interactive session, resume a conversation at launch with `--continue` or `--resume`, or run `/clear`, SessionStart hooks run in the background. You can type right away, and a conversation you resumed appears without waiting for the hooks. Claude’s first response still waits for the hooks to finish, so their context reaches Claude.When you switch conversations with `/resume` inside a session, the switch waits for the hooks to finish instead. If you run `/clear` or switch to another conversation while background hooks are still running, nothing they return applies to the session.The same wait applies at launch, including a resumed session: a prompt you send while SessionStart hooks are still running doesn’t reach Claude until they finish.During either wait, press `Esc` to take the prompt back into the input without sending it. The hooks keep running.
819
1052
 
820
1053
  #### [​](https://code.claude.com/docs/en/hooks\#sessionstart-input) SessionStart input
821
1054
 
@@ -823,10 +1056,21 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
823
1056
 
824
1057
  | Field | Description |
825
1058
  | --- | --- |
826
- | `source` | How the session started: `"startup"` for new sessions, `"resume"` for resumed sessions, `"clear"` after `/clear`, or `"compact"` after compaction |
1059
+ | `source` | How the session started: `"startup"` for new sessions, `"resume"` for resumed sessions, `"clear"` after `/clear`, `"compact"` after compaction, or `"fork"` for a new session forked from an existing one |
827
1060
  | `model` | The active model identifier. It can be omitted, for example after `/clear` or when a session is restored through conversation recovery, so check for the field before reading it |
828
1061
  | `agent_type` | The agent name, present when you start Claude Code with `claude --agent <name>` |
829
- | `session_title` | The current session title if one is already set, for example via `--name` or `/rename`. A hook that emits `sessionTitle` can check `session_title` first to avoid overwriting a title the user set explicitly |
1062
+ | `session_title` | The session’s custom title, present when one is set, for example with `--name`, `/rename`, a hook’s `sessionTitle` output, or the Agent SDK’s `renameSession()`. A hook that emits `sessionTitle` can check this field first to avoid overwriting an existing custom title |
1063
+
1064
+ A session you haven’t named can still have a [generated title](https://code.claude.com/docs/en/sessions#name-your-sessions). That title isn’t a custom title and doesn’t appear in `session_title`.When `source` is `"resume"` or `"fork"` and the transcript contains at least one response from Claude, SessionStart hooks also receive the four fields below. Your hook can use them to report what resuming a stale conversation costs before the first request, for example in a [`systemMessage`](https://code.claude.com/docs/en/hooks#json-output). These fields require Claude Code v2.1.251 or later.
1065
+
1066
+ | Field | Description |
1067
+ | --- | --- |
1068
+ | `seconds_since_last_response` | Wall-clock seconds since the last response in the resumed transcript |
1069
+ | `context_tokens` | Tokens the first request of the resumed session re-sends as its prompt |
1070
+ | `prompt_cache_likely_expired` | `true` when the last response is older than the session’s [prompt cache lifetime](https://code.claude.com/docs/en/prompt-caching#cache-lifetime) or a later compaction replaced the cached conversation |
1071
+ | `estimated_cache_write_usd` | Estimated cost in US dollars of writing `context_tokens` to the prompt cache on the session’s model, excluding the response |
1072
+
1073
+ This example shows the input for a session resumed 90 minutes after its last response:
830
1074
 
831
1075
  ```
832
1076
  {
@@ -834,22 +1078,28 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
834
1078
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
835
1079
  "cwd": "/Users/...",
836
1080
  "hook_event_name": "SessionStart",
837
- "source": "startup",
838
- "model": "claude-sonnet-5"
1081
+ "source": "resume",
1082
+ "model": "claude-opus-5",
1083
+ "seconds_since_last_response": 5400,
1084
+ "context_tokens": 182340,
1085
+ "prompt_cache_likely_expired": true,
1086
+ "estimated_cache_write_usd": 1.1396
839
1087
  }
840
1088
  ```
841
1089
 
842
1090
  #### [​](https://code.claude.com/docs/en/hooks\#sessionstart-decision-control) SessionStart decision control
843
1091
 
844
- Any text your hook script prints to stdout is added as context for Claude. In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) available to all hooks, you can return these event-specific fields:
1092
+ A SessionStart hook can add context for Claude, supply the first user message, set the session title, watch files, and reload skills. Return the field for each one, in addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) available to all hooks:
845
1093
 
846
1094
  | Field | Description |
847
1095
  | --- | --- |
848
1096
  | `additionalContext` | String added to Claude’s context at the start of the conversation, before the first prompt. See [Add context for Claude](https://code.claude.com/docs/en/hooks#add-context-for-claude) for how the text is delivered and what to put in it |
849
- | `initialUserMessage` | String used as the first user message of the session. Applies in [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag, where it becomes the first turn even if no prompt is provided. If a prompt is provided, it follows as the next turn. Unlike `additionalContext`, which attaches to an existing turn, this creates the turn |
850
- | `sessionTitle` | Sets the session title, with the same effect as `/rename`. Use to name sessions automatically from the launch folder, git branch, or worktree name. Applies only when `source` is `"startup"` or `"resume"`; ignored on `"clear"` and `"compact"` |
1097
+ | `initialUserMessage` | String used as the first user message of the session, in [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag. It becomes the first turn even if you pass no prompt. A prompt you do pass follows as the next turn |
1098
+ | `sessionTitle` | Sets the session title, with the same effect as `/rename`. Applies when `source` is `"startup"`, `"resume"`, or `"fork"` |
851
1099
  | `watchPaths` | Array of absolute paths to watch for [FileChanged](https://code.claude.com/docs/en/hooks#filechanged) events during this session |
852
- | `reloadSkills` | Boolean. When `true`, Claude Code re-scans the [skill](https://code.claude.com/docs/en/skills) and command directories after the SessionStart hooks complete, so skills the hook installed are available in the same session, starting with the first prompt |
1100
+ | `reloadSkills` | Boolean. When `true`, Claude Code re-scans the [skill](https://code.claude.com/docs/en/skills) and command directories after the SessionStart hooks complete. See [Reload skills that a hook installs](https://code.claude.com/docs/en/hooks#reload-skills-that-a-hook-installs) |
1101
+
1102
+ This output adds context and names the session:
853
1103
 
854
1104
  ```
855
1105
  {
@@ -861,7 +1111,11 @@ Any text your hook script prints to stdout is added as context for Claude. In ad
861
1111
  }
862
1112
  ```
863
1113
 
864
- Since plain stdout already reaches Claude for this event, a hook that only loads context can print to stdout directly without building JSON. Use the JSON form when you need to combine context with other fields such as `suppressOutput` or `sessionTitle`.Use `reloadSkills` when a SessionStart hook installs or updates skills. Skill discovery normally runs before SessionStart hooks finish, so files the hook writes into `~/.claude/skills/` or `.claude/skills/` would otherwise only appear in the next session. This example syncs a shared skills repository and requests the re-scan:
1114
+ A hook that only adds context can print it without building JSON, because Claude Code adds a SessionStart hook’s [plain-text stdout](https://code.claude.com/docs/en/hooks#exit-code-0) to Claude’s context.If your plugin’s SessionStart hook supplies `initialUserMessage` or `sessionTitle`, install the plugin before the session starts. Claude Code ignores both fields from a plugin that finishes installing after the SessionStart hooks have run.
1115
+
1116
+ #### [​](https://code.claude.com/docs/en/hooks\#reload-skills-that-a-hook-installs) Reload skills that a hook installs
1117
+
1118
+ To make skills that a SessionStart hook installs available in the same session, return `reloadSkills`. Skill discovery normally runs before SessionStart hooks finish, so without it, files a hook writes into `~/.claude/skills/` or `.claude/skills/` can be missing when the first prompt runs.This example syncs a shared skills repository and requests the re-scan:
865
1119
 
866
1120
  ```
867
1121
  #!/bin/bash
@@ -872,9 +1126,11 @@ git -C ~/.claude/skills/team-skills pull --quiet 2>/dev/null || \
872
1126
  echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "reloadSkills": true}}'
873
1127
  ```
874
1128
 
1129
+ The repository URL is a placeholder. Replace it with your own skills repository.
1130
+
875
1131
  #### [​](https://code.claude.com/docs/en/hooks\#persist-environment-variables) Persist environment variables
876
1132
 
877
- SessionStart hooks have access to the `CLAUDE_ENV_FILE` environment variable, which provides a file path where you can persist environment variables for subsequent Bash commands.To set individual environment variables, write `export` statements to `CLAUDE_ENV_FILE`. Use append (`>>`) to preserve variables set by other hooks:
1133
+ SessionStart hooks have access to the `CLAUDE_ENV_FILE` environment variable, which provides a file path where you can persist environment variables for the shell commands Claude runs later in the session.To set individual environment variables, write `export` statements to `CLAUDE_ENV_FILE`. Use append (`>>`) to preserve variables set by other hooks:
878
1134
 
879
1135
  ```
880
1136
  #!/bin/bash
@@ -907,9 +1163,18 @@ fi
907
1163
  exit 0
908
1164
  ```
909
1165
 
910
- Any variables written to this file will be available in all subsequent Bash commands that Claude Code executes during the session.
1166
+ Each 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"`.
1167
+
1168
+ ##### Persisted variables in PowerShell commands
911
1169
 
912
- `CLAUDE_ENV_FILE` is available for SessionStart, [Setup](https://code.claude.com/docs/en/hooks#setup), [CwdChanged](https://code.claude.com/docs/en/hooks#cwdchanged), and [FileChanged](https://code.claude.com/docs/en/hooks#filechanged) hooks. Other hook types don’t have access to this variable.
1170
+ [PowerShell](https://code.claude.com/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](https://code.claude.com/docs/en/env-vars) before launch:
1171
+
1172
+ - A blank line or a `#` comment
1173
+ - One assignment at the start of the line, written `export NAME=value`, `declare -x NAME=value`, or `NAME=value`, with a value Bash would use exactly as written, made of any mix of these parts: unquoted text that uses only letters, digits, and the characters `_ @ % + = : , . / -`, text in single quotes, and text in double quotes where any `$`, backtick, or `"` inside is escaped with a backslash
1174
+
1175
+ If 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](https://code.claude.com/docs/en/sandboxing) PowerShell command receives none of the variables.
1176
+
1177
+ `CLAUDE_ENV_FILE` is available for SessionStart, [Setup](https://code.claude.com/docs/en/hooks#setup), [CwdChanged](https://code.claude.com/docs/en/hooks#cwdchanged), and [FileChanged](https://code.claude.com/docs/en/hooks#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"`](https://code.claude.com/docs/en/hooks#command-hook-fields) or by default on Windows without Git Bash.
913
1178
 
914
1179
  ### [​](https://code.claude.com/docs/en/hooks\#setup) Setup
915
1180
 
@@ -920,7 +1185,7 @@ Fires only when you launch Claude Code with `--init-only`, or with `--init` or `
920
1185
  | `init` | `claude --init-only` or `claude -p --init` |
921
1186
  | `maintenance` | `claude -p --maintenance` |
922
1187
 
923
- `--init-only` runs Setup hooks and `SessionStart` hooks with the `startup` matcher, then exits without starting a conversation. `--init` and `--maintenance` fire Setup hooks only when combined with `-p`; in an interactive session those two flags don’t currently fire Setup hooks.Because Setup doesn’t fire on every launch, a plugin that needs a dependency installed can’t rely on Setup alone. The practical pattern is to check for the dependency on first use and install on miss, for example a hook or skill that tests for `${CLAUDE_PLUGIN_DATA}/node_modules` and runs `npm install` if absent. See the [persistent data directory](https://code.claude.com/docs/en/plugins-reference#persistent-data-directory) for where to store installed dependencies.
1188
+ When you run `claude --init-only`, Claude Code runs Setup hooks and `SessionStart` hooks with the `startup` matcher, then exits without starting a conversation.When you start or continue a conversation with `-p`, you also need to supply a prompt, as an argument or piped on stdin. You can skip the prompt when a `SessionStart` hook supplies [`initialUserMessage`](https://code.claude.com/docs/en/hooks#sessionstart-decision-control) or when you resume a session with a [deferred tool call](https://code.claude.com/docs/en/hooks#defer-a-tool-call-for-later).On success, `--init-only` prints nothing to the terminal. To confirm the hooks ran, start with `claude --debug-file <path> --init-only`, replacing `<path>` with a log file location, and check the log for the Setup and SessionStart hook entries.Because Setup doesn’t fire on every launch, a plugin that needs a dependency installed can’t rely on Setup alone. The practical pattern is to check for the dependency on first use and install on miss, for example a hook or skill that tests for `${CLAUDE_PLUGIN_DATA}/node_modules` and runs `npm install` if absent. See the [persistent data directory](https://code.claude.com/docs/en/plugins/components#path-variables-and-persistent-data) for where to store installed dependencies. If you distribute your plugin through a marketplace, you may not need this pattern: Claude Code [installs eligible Node.js package dependencies automatically](https://code.claude.com/docs/en/plugins/loading#node-js-package-dependencies) when it caches the plugin.
924
1189
 
925
1190
  #### [​](https://code.claude.com/docs/en/hooks\#setup-input) Setup input
926
1191
 
@@ -938,26 +1203,11 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
938
1203
 
939
1204
  #### [​](https://code.claude.com/docs/en/hooks\#setup-decision-control) Setup decision control
940
1205
 
941
- Setup hooks can’t block. Any non-zero exit code, including 2, surfaces stderr to the user as a `<hook name> hook error` notice, and execution continues. In [non-interactive mode](https://code.claude.com/docs/en/headless), hook output appears only when you launch with `--verbose`.To pass information into Claude’s context, return `additionalContext` in JSON output; plain stdout is written to the debug log only. In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) available to all hooks, you can return these event-specific fields:
942
-
943
- | Field | Description |
944
- | --- | --- |
945
- | `additionalContext` | String added to Claude’s context. Multiple hooks’ values are concatenated |
946
-
947
- ```
948
- {
949
- "hookSpecificOutput": {
950
- "hookEventName": "Setup",
951
- "additionalContext": "Dependencies installed: node_modules, .venv"
952
- }
953
- }
954
- ```
955
-
956
- Setup hooks have access to `CLAUDE_ENV_FILE`. Variables written to that file persist into subsequent Bash commands for the session, just as in [SessionStart hooks](https://code.claude.com/docs/en/hooks#persist-environment-variables). Only `type: "command"` and `type: "mcp_tool"` hooks are supported.
1206
+ Setup hooks can’t block; execution continues on any exit code. On every exit code, Claude Code discards a Setup hook’s [JSON output fields](https://code.claude.com/docs/en/hooks#json-output), such as `systemMessage`, `continue`, and `hookSpecificOutput.additionalContext`. With `-p`, a Setup hook’s stdout, stderr, and exit code appear in the run’s output only as [`hook_response` events](https://code.claude.com/docs/en/headless#read-session-metadata) when you launch with `--output-format stream-json --verbose`.Setup hooks have access to `CLAUDE_ENV_FILE`. Variables written to that file persist into subsequent Bash commands for the session, as in [SessionStart hooks](https://code.claude.com/docs/en/hooks#persist-environment-variables). Only `type: "command"` hooks run on `Setup`. A `type: "mcp_tool"` hook on `Setup` is always skipped, as described under [MCP tool hook fields](https://code.claude.com/docs/en/hooks#mcp-tool-hook-fields).
957
1207
 
958
1208
  ### [​](https://code.claude.com/docs/en/hooks\#instructionsloaded) InstructionsLoaded
959
1209
 
960
- Fires when a `CLAUDE.md` or `.claude/rules/*.md` file is loaded into context. This event fires at session start for eagerly-loaded files and again later when files are lazily loaded, for example when Claude accesses a subdirectory that contains a nested `CLAUDE.md` or when conditional rules with `paths:` frontmatter match. The hook doesn’t support blocking or decision control. It runs asynchronously for observability purposes.The matcher runs against `load_reason`. For example, use `"matcher": "session_start"` to fire only for files loaded at session start, or `"matcher": "path_glob_match|nested_traversal"` to fire only for lazy loads.
1210
+ Fires when a `CLAUDE.md` or `.claude/rules/*.md` file is loaded into context. This event fires at session start for eagerly loaded files and again later when files are lazily loaded, for example when Claude accesses a subdirectory that contains a nested `CLAUDE.md` or when conditional rules with `paths:` frontmatter match. The hook doesn’t support blocking or decision control. It runs asynchronously for observability purposes.This event doesn’t fire when Claude [reads `AGENTS.md` directly](https://code.claude.com/docs/en/memory#agents-md) through the **Project instructions** setting. It does fire when a `CLAUDE.md` imports your `AGENTS.md`, with `load_reason` set to `include` as for any other imported file, and when `CLAUDE.md` is a symlink to it, as a normal `CLAUDE.md` load.The matcher runs against `load_reason`. For example, use `"matcher": "session_start"` to fire only for files loaded at session start, or `"matcher": "path_glob_match|nested_traversal"` to fire only for lazy loads.
961
1211
 
962
1212
  #### [​](https://code.claude.com/docs/en/hooks\#instructionsloaded-input) InstructionsLoaded input
963
1213
 
@@ -986,17 +1236,23 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
986
1236
 
987
1237
  #### [​](https://code.claude.com/docs/en/hooks\#instructionsloaded-decision-control) InstructionsLoaded decision control
988
1238
 
989
- InstructionsLoaded hooks have no decision control. They can’t block or modify instruction loading. Use this event for audit logging, compliance tracking, or observability.
1239
+ InstructionsLoaded hooks have no decision control. They can’t block or modify instruction loading. Claude Code discards their [JSON output fields](https://code.claude.com/docs/en/hooks#json-output), such as `systemMessage` and `continue`. Use this event for audit logging, compliance tracking, or observability.
990
1240
 
991
1241
  ### [​](https://code.claude.com/docs/en/hooks\#userpromptsubmit) UserPromptSubmit
992
1242
 
993
- Runs when the user submits a prompt, before Claude processes it. This allows you
1243
+ Runs when a prompt is submitted, before Claude processes it. This allows you
994
1244
  to add additional context based on the prompt/conversation, validate prompts, or
995
- block certain types of prompts.`UserPromptSubmit` hooks have a default timeout of 30 seconds for `command`, `http`, and `mcp_tool` types, shorter than the 600-second default for those types on most other events. Because this hook runs before every prompt and blocks model processing until it completes, a stuck hook stalls the session. If your hook needs more time, set the `timeout` field in the hook entry.A `UserPromptSubmit` hook that reaches its timeout is canceled and its output, including any `additionalContext`, is discarded. The prompt still reaches Claude without that context. As of v2.1.196, the transcript shows a notice naming the hook, the timeout that fired, and that the output was discarded. Earlier versions cancel the hook with no notice.
1245
+ block certain types of prompts.`UserPromptSubmit` hooks don’t fire only on prompts you type. Claude Code also runs them on:
1246
+
1247
+ - A [scheduled task](https://code.claude.com/docs/en/scheduled-tasks) firing, including a `/loop` iteration
1248
+ - A [background subagent](https://code.claude.com/docs/en/sub-agents#run-subagents-in-foreground-or-background) reporting back to the session that started it
1249
+ - A [message another session sends](https://code.claude.com/docs/en/cross-session-messaging) to your main conversation
1250
+
1251
+ `UserPromptSubmit` hooks have a default timeout of 30 seconds for `command`, `http`, and `mcp_tool` types, shorter than the 600-second default for those types on most other events. Because this hook runs before every prompt and blocks model processing until it completes, a stuck hook stalls the session. If your hook needs more time, set the `timeout` field in the hook entry.Apart from a command hook you run with [`async: true`](https://code.claude.com/docs/en/hooks#run-hooks-in-the-background), a `UserPromptSubmit` command, HTTP, or MCP tool hook that reaches its timeout is canceled and its output, including any `additionalContext`, is discarded. The prompt still reaches Claude without that context. To block the prompt instead, set [`onFailure: "block"`](https://code.claude.com/docs/en/hooks#block-the-action-when-a-hook-fails) on a command or HTTP hook. The transcript shows a notice naming the hook, the timeout that fired, and that the output was discarded.An [Agent SDK callback hook](https://code.claude.com/docs/en/agent-sdk/hooks) on `UserPromptSubmit` that reaches its timeout blocks the prompt with a message naming the hook and the timeout, because a callback there can be acting as a policy gate that must not fail open. The session continues. Before v2.1.208, a callback timeout on that event ended the turn with an execution error.
996
1252
 
997
1253
  #### [​](https://code.claude.com/docs/en/hooks\#userpromptsubmit-input) UserPromptSubmit input
998
1254
 
999
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), UserPromptSubmit hooks receive the `prompt` field containing the text the user submitted.
1255
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), UserPromptSubmit hooks receive the `prompt` field containing the submitted text. Pasted content that collapsed to a `[Pasted text #N]` placeholder arrives expanded in place. In sessions where Claude Code [marks pasted text for Claude](https://code.claude.com/docs/en/terminal-config#how-claude-treats-pasted-text), that expanded content sits between a `<pasted_content id="…">` line and a `</pasted_content id="…">` line, so account for those lines if your hook parses the prompt.UserPromptSubmit hooks also receive `session_title` when the session has a custom title, with the same meaning as the [SessionStart `session_title` field](https://code.claude.com/docs/en/hooks#sessionstart-input).
1000
1256
 
1001
1257
  ```
1002
1258
  {
@@ -1011,20 +1267,22 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1011
1267
 
1012
1268
  #### [​](https://code.claude.com/docs/en/hooks\#userpromptsubmit-decision-control) UserPromptSubmit decision control
1013
1269
 
1014
- `UserPromptSubmit` hooks can control whether a user prompt is processed and add context. All [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) are available.There are two ways to add context to the conversation on exit code 0:
1270
+ `UserPromptSubmit` hooks can control whether a submitted prompt is processed and add context. All [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) are available.There are two ways to add context to the conversation on exit code 0:
1015
1271
 
1016
- - **Plain text stdout**: any non-JSON text written to stdout is added as context
1272
+ - **Plain text stdout**: Claude Code adds stdout it [treats as plain text](https://code.claude.com/docs/en/hooks#exit-code-0) to Claude’s context
1017
1273
  - **JSON with `additionalContext`**: use the JSON format below for more control. The `additionalContext` field is added as context
1018
1274
 
1019
- Plain stdout is shown as hook output in the transcript. The `additionalContext` value is injected as a system reminder that Claude reads without a visible transcript entry.To block a prompt, return a JSON object with `decision` set to `"block"`:
1275
+ Neither channel produces a visible transcript entry. Plain stdout and the `additionalContext` value are each injected as a system reminder that starts with the hook’s name; Claude reads both. To confirm delivery, check the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks).To block a prompt, return a JSON object with `decision` set to `"block"`:
1020
1276
 
1021
1277
  | Field | Description |
1022
1278
  | --- | --- |
1023
- | `decision` | `"block"` prevents the prompt from being processed and erases it from context. Omit to allow the prompt to proceed |
1279
+ | `decision` | `"block"` stops the prompt before it reaches Claude. Omit to allow the prompt to proceed |
1024
1280
  | `reason` | Shown to the user when `decision` is `"block"`. Not added to context |
1025
1281
  | `additionalContext` | String added to Claude’s context alongside the submitted prompt. See [Add context for Claude](https://code.claude.com/docs/en/hooks#add-context-for-claude) |
1026
1282
  | `sessionTitle` | Sets the session title. Use to name sessions automatically based on the prompt content |
1027
- | `suppressOriginalPrompt` | If `true` when `decision` is `"block"`, omits the original prompt text from the block message shown to the user |
1283
+ | `suppressOriginalPrompt` | If `true` when the hook blocks the prompt, leaves the prompt text out of the block message. See [What a blocked prompt leaves behind](https://code.claude.com/docs/en/hooks#what-a-blocked-prompt-leaves-behind) |
1284
+
1285
+ A hook that blocks by exiting 2 routes the same way as `reason`: the block message shows the stderr text to the user, and it isn’t added to context.
1028
1286
 
1029
1287
  ```
1030
1288
  {
@@ -1033,11 +1291,16 @@ Plain stdout is shown as hook output in the transcript. The `additionalContext`
1033
1291
  "hookSpecificOutput": {
1034
1292
  "hookEventName": "UserPromptSubmit",
1035
1293
  "additionalContext": "My additional context here",
1036
- "sessionTitle": "My session title"
1294
+ "sessionTitle": "My session title",
1295
+ "suppressOriginalPrompt": true
1037
1296
  }
1038
1297
  }
1039
1298
  ```
1040
1299
 
1300
+ #### [​](https://code.claude.com/docs/en/hooks\#what-a-blocked-prompt-leaves-behind) What a blocked prompt leaves behind
1301
+
1302
+ A blocked prompt never reaches Claude, but its text isn’t removed everywhere. By default the block message shown to the user ends with `Original prompt:` followed by the submitted text, and Claude Code writes that message to the session’s transcript file on disk. To leave the text out of the message, print JSON with `"suppressOriginalPrompt": true` inside `hookSpecificOutput`. This works whether the hook blocks with `decision: "block"` or by exiting 2.`suppressOriginalPrompt` changes only the block message. The submitted text can still appear in local files such as the session transcript and your prompt history, so a blocking hook isn’t a way to keep a secret off disk. To limit or remove those files, see [Plaintext storage](https://code.claude.com/docs/en/claude-directory#plaintext-storage) and [Clear local data](https://code.claude.com/docs/en/claude-directory#clear-local-data).
1303
+
1041
1304
  ### [​](https://code.claude.com/docs/en/hooks\#userpromptexpansion) UserPromptExpansion
1042
1305
 
1043
1306
  Runs when a user-typed command expands into a prompt before reaching Claude. Use this to block specific commands from direct invocation, inject context for a particular skill, or log which commands users invoke. For example, a hook matching `deploy` can block `/deploy` unless an approval file is present, or a hook matching a review skill can append the team’s review checklist as `additionalContext`.This event covers the path `PreToolUse` doesn’t: a `PreToolUse` hook matching the `Skill` tool fires only when Claude calls the tool, but typing `/skillname` directly bypasses `PreToolUse`. `UserPromptExpansion` fires on that direct path.Matches on `command_name`. Leave the matcher empty to fire on every prompt-type command.
@@ -1071,6 +1334,8 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1071
1334
  | `reason` | Shown to the user when `decision` is `"block"` |
1072
1335
  | `additionalContext` | String added to Claude’s context alongside the expanded prompt. See [Add context for Claude](https://code.claude.com/docs/en/hooks#add-context-for-claude) |
1073
1336
 
1337
+ A hook that blocks by exiting 2 routes the same way as `reason`: the block message shows the stderr text to the user.
1338
+
1074
1339
  ```
1075
1340
  {
1076
1341
  "decision": "block",
@@ -1126,7 +1391,7 @@ In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#js
1126
1391
  | --- | --- |
1127
1392
  | `displayContent` | Text displayed in place of the delta. Omit it to display the original |
1128
1393
 
1129
- MessageDisplay hooks have no decision control. They can’t block the message or change what is stored in the transcript or sent to Claude.This 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`.
1394
+ MessageDisplay hooks have no decision control. They can’t block the message or change what is stored in the transcript or sent to Claude. Claude Code acts on `displayContent` from their JSON output and discards `systemMessage` and `continue`.This 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`.
1130
1395
 
1131
1396
  - macOS/Linux
1132
1397
 
@@ -1160,8 +1425,6 @@ Save this script to `.claude/hooks/plain-display.sh` in your project and make it
1160
1425
  jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'
1161
1426
  ```
1162
1427
 
1163
- The script needs `jq` on your `PATH`.
1164
-
1165
1428
  Register a command hook that runs the script through PowerShell:
1166
1429
 
1167
1430
  ```
@@ -1205,15 +1468,36 @@ Batches with no markdown pass through unchanged. If the script fails, for exampl
1205
1468
 
1206
1469
  ### [​](https://code.claude.com/docs/en/hooks\#pretooluse) PreToolUse
1207
1470
 
1208
- Runs after Claude creates tool parameters and before processing the tool call. Matches on tool name: `Bash`, `Edit`, `Write`, `Read`, `Glob`, `Grep`, `Agent`, `WebFetch`, `WebSearch`, `AskUserQuestion`, `ExitPlanMode`, and any [MCP tool names](https://code.claude.com/docs/en/hooks#match-mcp-tools).
1471
+ Runs after Claude creates tool parameters and before processing the tool call. Matches on any tool name except `EndConversation`: built-in tools such as `Bash`, `PowerShell`, `Edit`, `Write`, `Read`, `Glob`, `Grep`, `Agent`, `Workflow`, `WebFetch`, `WebSearch`, `AskUserQuestion`, and `ExitPlanMode`, and any [MCP tool names](https://code.claude.com/docs/en/hooks#match-mcp-tools).To run a hook when a specific file changes on disk, whatever wrote it, use [FileChanged](https://code.claude.com/docs/en/hooks#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.
1209
1472
 
1210
- PreToolUse runs only when Claude calls a tool. Files you [reference with `@` in your prompt](https://code.claude.com/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`. To block specific paths from `@` references, use a [`Read` deny rule](https://code.claude.com/docs/en/permissions#read-and-edit) instead.
1473
+ PreToolUse runs only when Claude calls a tool. Files you [reference with `@` in your prompt](https://code.claude.com/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`. To block specific paths from `@` references, use a [`Read` deny rule](https://code.claude.com/docs/en/permissions#read-and-edit) instead.PreToolUse also doesn’t fire for [`EndConversation`](https://code.claude.com/docs/en/tools-reference#endconversation-tool-behavior).
1211
1474
 
1212
- Use [PreToolUse decision control](https://code.claude.com/docs/en/hooks#pretooluse-decision-control) to allow, deny, ask, or defer the tool call.
1475
+ Use [PreToolUse decision control](https://code.claude.com/docs/en/hooks#pretooluse-decision-control) to allow, deny, ask, or defer the tool call.An [Agent SDK callback hook](https://code.claude.com/docs/en/agent-sdk/hooks) on `PreToolUse` that exceeds its timeout blocks the tool call, and Claude receives an error result naming the timeout. An explicit deny returned by another hook still takes precedence.
1213
1476
 
1214
1477
  #### [​](https://code.claude.com/docs/en/hooks\#pretooluse-input) PreToolUse input
1215
1478
 
1216
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PreToolUse hooks receive `tool_name`, `tool_input`, and `tool_use_id`. The `tool_input` fields depend on the tool:
1479
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PreToolUse hooks receive `tool_name`, `tool_input`, and `tool_use_id`.For an [MCP tool](https://code.claude.com/docs/en/hooks#match-mcp-tools), the input also carries `mcp_server`, an object with the server’s `name` and a `source` that says where the server’s definition came from. The `source` values include `plugin`, `sdk`, and configuration scopes such as `user` and `project`. [`McpServerProvenance`](https://code.claude.com/docs/en/agent-sdk/typescript#mcpserverprovenance) in the Agent SDK reference lists them all and says how to treat one you don’t recognize. Base trust decisions on `source` rather than on `name` or the `mcp__<server>__` tool-name prefix. The `mcp_server` field requires Claude Code v2.1.274 or later.For the file tools `Write`, `Edit`, and `Read`, `tool_input.file_path` is always absolute:
1480
+
1481
+ - Claude Code expands `~` and relative paths before hooks run, so a hook that matches on paths can’t be bypassed via `~` or a relative spelling of the same path
1482
+ - On Windows, the path arrives with backslash separators, even when your hook runs under Git Bash where `$PWD` looks like `/c/project`
1483
+ - A comparison written with forward slashes, such as a `/src/` check, never matches a backslash path, and the tool call proceeds as if the hook had nothing to block
1484
+ - Normalize separators before comparing: `FILE_PATH="${FILE_PATH//\\//}"` in Bash, or `file_path.replace("\\", "/")` in Python, then match a path segment such as `/src/` rather than anchoring with `^`, since the path is absolute
1485
+
1486
+ A `Write` call on Windows delivers:
1487
+
1488
+ ```
1489
+ {
1490
+ "hook_event_name": "PreToolUse",
1491
+ "tool_name": "Write",
1492
+ "tool_input": {
1493
+ "file_path": "C:\\project\\src\\index.ts",
1494
+ "content": "..."
1495
+ },
1496
+ ...
1497
+ }
1498
+ ```
1499
+
1500
+ The `tool_input` fields depend on the tool:
1217
1501
 
1218
1502
  ##### Bash
1219
1503
 
@@ -1223,9 +1507,41 @@ Executes shell commands.
1223
1507
  | --- | --- | --- | --- |
1224
1508
  | `command` | string | `"npm test"` | The shell command to execute |
1225
1509
  | `description` | string | `"Run test suite"` | Optional description of what the command does |
1510
+ | `timeout` | number | `120000` | Optional timeout in milliseconds. Values above the [maximum](https://code.claude.com/docs/en/tools-reference#bash-tool-behavior) are reduced to the maximum rather than rejected |
1511
+ | `run_in_background` | boolean | `false` | Whether to run the command in background |
1512
+
1513
+ When a Bash command changes files in a Git repository, Claude Code can record what changed. It records the changes in every permission mode when the [`bashEditDiffEnabled`](https://code.claude.com/docs/en/settings-reference#basheditdiffenabled) setting turns recording on; that setting’s entry says which files can set it. Otherwise it records them only in auto mode and `bypassPermissions` mode, and only when Claude Code directs Claude to edit files through Bash. Set `bashEditDiffEnabled` to `false` to turn the recording off. Background commands and read-only commands carry no diff.Your [PostToolUse hook](https://code.claude.com/docs/en/hooks#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.
1514
+
1515
+ 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.
1516
+
1517
+ `changedFiles` and `files` list what the command changed; the remaining fields say how complete and how reliable that list is.
1518
+
1519
+ | Field | Type | Example | Description |
1520
+ | --- | --- | --- | --- |
1521
+ | `changedFiles` | array | `["/path/to/src/app.ts"]` | Absolute paths of the files the command changed, at most 200. Present whenever `files` holds a diff or `moreFiles` is above zero |
1522
+ | `files` | array | `[{"filePath": "/path/to/src/app.ts", "hunks": [...]}]` | Diffs of up to 5 changed files, for display. `created` or `deleted` is `true` for a file the command added or removed |
1523
+ | `moreFiles` | number | `2` | Count of changed files with no diff in `files` |
1524
+ | `unavailable` | boolean | `true` | Set when the diff is incomplete or couldn’t be taken |
1525
+ | `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 |
1526
+ | `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 |
1527
+
1528
+ ##### PowerShell
1529
+
1530
+ Executes PowerShell commands. See the [PowerShell tool](https://code.claude.com/docs/en/tools-reference#powershell-tool) for availability by platform.The fields match the Bash tool, with the command string in `command`:
1531
+
1532
+ | Field | Type | Example | Description |
1533
+ | --- | --- | --- | --- |
1534
+ | `command` | string | `"Get-ChildItem -Recurse"` | The PowerShell command to execute |
1535
+ | `description` | string | `"List files recursively"` | Optional description of what the command does |
1226
1536
  | `timeout` | number | `120000` | Optional timeout in milliseconds |
1227
1537
  | `run_in_background` | boolean | `false` | Whether to run the command in background |
1228
1538
 
1539
+ Match `Bash|PowerShell` in hooks that inspect shell commands, so they cover both tools:
1540
+
1541
+ - On Windows, wherever the PowerShell tool is enabled, Claude treats PowerShell as the primary shell and routes shell commands through it.
1542
+ - On Windows without Git Bash, the tool is enabled automatically and Claude Code doesn’t register the Bash tool at all.
1543
+ - A hook that matches only `Bash` never fires there.
1544
+
1229
1545
  ##### Write
1230
1546
 
1231
1547
  Creates or overwrites a file.
@@ -1286,6 +1602,7 @@ Fetches and processes web content.
1286
1602
  | --- | --- | --- | --- |
1287
1603
  | `url` | string | `"https://example.com/api"` | URL to fetch content from |
1288
1604
  | `prompt` | string | `"Extract the API endpoints"` | Prompt to run on the fetched content |
1605
+ | `offset` | number | `100000` | Optional number of characters to skip from the start of the page. Claude sets it to keep reading a long page. Requires Claude Code v2.1.290 or later |
1289
1606
 
1290
1607
  ##### WebSearch
1291
1608
 
@@ -1308,20 +1625,21 @@ Spawns a [subagent](https://code.claude.com/docs/en/sub-agents).
1308
1625
  | `subagent_type` | string | `"Explore"` | Type of specialized agent to use |
1309
1626
  | `model` | string | `"sonnet"` | Optional model alias to override the default |
1310
1627
 
1311
- In `PostToolUse`, `tool_response` for a completed Agent call carries the subagent’s final text along with usage telemetry. Read these fields to record per-subagent cost from a hook:
1628
+ When a foreground Agent call completes, your [PostToolUse hook](https://code.claude.com/docs/en/hooks#posttooluse) receives the subagent’s result and run telemetry in `tool_response`. Read these fields to inspect the run; for token and cost rollups across subagents, use the [token and cost counters](https://code.claude.com/docs/en/monitoring-usage#token-counter) filtered to `query_source``"subagent"`, since `totalTokens` and `usage` cover the final request only:
1312
1629
 
1313
1630
  | Field | Type | Example | Description |
1314
1631
  | --- | --- | --- | --- |
1315
- | `status` | string | `"completed"` | `"completed"` for foreground subagents, `"async_launched"` for background subagents. As of v2.1.198, subagents run in the background by default, so an omitted `run_in_background` also produces `"async_launched"` |
1632
+ | `status` | string | `"completed"` | `"completed"` for foreground subagents, `"async_launched"` for background subagents. Subagents run in the background by default, so an Agent call that omits `run_in_background` also produces `"async_launched"` |
1316
1633
  | `agentId` | string | `"a4d2c8f1e0b3a297"` | Identifier for the subagent run |
1317
- | `content` | array | `[{"type": "text", "text": "Found 12 endpoints..."}]` | The subagent’s final text blocks |
1318
- | `resolvedModel` | string | `"claude-sonnet-4-5"` | Model the subagent ran on, which may differ from the requested model. Requires Claude Code v2.1.174 or later |
1319
- | `totalTokens` | number | `12450` | Total tokens billed across the subagent’s turns |
1634
+ | `content` | array | `[{"type": "text", "text": "Found 12 endpoints..."}]` | The subagent’s final text blocks, or, for a subagent whose report goes through `SubagentHandback`, a short note about that hand-back in their place |
1635
+ | `resolvedModel` | string | `"claude-sonnet-4-5"` | Model the subagent started on, which may differ from the requested model |
1636
+ | `modelsUsed` | array | `["claude-sonnet-4-5", "claude-haiku-4-5"]` | Models used in order, with consecutive repeats collapsed; set only when the model was swapped mid-run. Requires Claude Code v2.1.212 or later |
1637
+ | `totalTokens` | number | `12450` | Token count from the subagent’s final API request: input, output, and cache tokens combined. This isn’t a total across the whole run |
1320
1638
  | `totalDurationMs` | number | `48211` | Wall-clock duration of the subagent run |
1321
1639
  | `totalToolUseCount` | number | `7` | Count of tool calls the subagent made |
1322
- | `usage` | object | `{"input_tokens": 8320, ...}` | Per-type token breakdown: `input_tokens`, `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens` |
1640
+ | `usage` | object | `{"input_tokens": 8320, ...}` | Per-type token breakdown of the final API request: `input_tokens`, `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens` |
1323
1641
 
1324
- For background subagents, the tool returns immediately after launching, so `tool_response` carries no usage fields. It has `status: "async_launched"`, `agentId`, `description`, `prompt`, `outputFile`, and `resolvedModel`.The `resolvedModel` field names the model the subagent actually runs on, which can differ from the `model` value in `tool_input`, such as when `availableModels` or another override applies. It requires Claude Code v2.1.174 or later.
1642
+ On Claude Code v2.1.271 or later, a subagent that runs with the [`SubagentHandback`](https://code.claude.com/docs/en/tools-reference) tool, which Claude Code provides in [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode), delivers its report through that tool rather than returning it as text. The `content` field of its `completed` result then carries a short note about that hand-back rather than the report itself. To read the report, match a `PreToolUse` or `PostToolUse` hook on `SubagentHandback` and read `tool_input.message`.For background subagents, the tool returns when the task moves to the background, so `tool_response` carries no usage fields: a background launch returns immediately, and a foreground task that Claude Code backgrounds mid-run returns at that transition. It has `status: "async_launched"`, `agentId`, `description`, `prompt`, `outputFile`, and `resolvedModel`.On 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. `modelsUsed` and the backgrounding-time `resolvedModel` behavior require Claude Code v2.1.212 or later.
1325
1643
 
1326
1644
  ##### AskUserQuestion
1327
1645
 
@@ -1329,7 +1647,7 @@ Asks the user one to four multiple-choice questions.
1329
1647
 
1330
1648
  | Field | Type | Example | Description |
1331
1649
  | --- | --- | --- | --- |
1332
- | `questions` | array | `[{"question": "Which framework?", "header": "Framework", "options": [{"label": "React"}], "multiSelect": false}]` | Questions to present, each with a `question` string, short `header`, `options` array, and optional `multiSelect` flag |
1650
+ | `questions` | array | `[{"question": "Which framework?", "header": "Framework", "options": [{"label": "React", "description": "Component library"}, {"label": "Vue", "description": "Progressive framework"}], "multiSelect": false}]` | Questions to present, each with a `question` string, short `header`, `options` array, and optional `multiSelect` flag |
1333
1651
  | `answers` | object | `{"Which framework?": "React"}` | Optional. Maps question text to the selected option label. Multi-select answers join labels with commas. Claude doesn’t set this field; supply it via `updatedInput` to answer programmatically |
1334
1652
 
1335
1653
  ##### ExitPlanMode
@@ -1350,12 +1668,12 @@ In `PostToolUse`, `tool_response` is an object with `plan` and `filePath` fields
1350
1668
 
1351
1669
  | Field | Description |
1352
1670
  | --- | --- |
1353
- | `permissionDecision` | `"allow"` skips the permission prompt, except for [tools that require user interaction](https://code.claude.com/docs/en/hooks#pretooluse-decision-control). `"deny"` prevents the tool call. `"ask"` prompts the user to confirm. `"defer"` exits gracefully so the tool can be resumed later. [Deny and ask rules](https://code.claude.com/docs/en/permissions#manage-permissions) are still evaluated regardless of what the hook returns |
1354
- | `permissionDecisionReason` | For `"allow"` and `"ask"`, shown to the user but not Claude. For `"deny"`, shown to Claude. For `"defer"`, ignored |
1355
- | `updatedInput` | Modifies the tool’s input parameters before execution. Replaces the entire input object, so include unchanged fields alongside modified ones. Combine with `"allow"` to auto-approve, or `"ask"` to show the modified input to the user. For `"defer"`, ignored |
1671
+ | `permissionDecision` | `"allow"` skips the permission prompt, except for the [actions no mode auto-approves](https://code.claude.com/docs/en/permission-modes#actions-no-mode-auto-approves), for [reads from network paths](https://code.claude.com/docs/en/permissions#network-paths), and for `AskUserQuestion` and `ExitPlanMode`, which need [`updatedInput` paired with it](https://code.claude.com/docs/en/hooks#allow-with-updatedinput). `"deny"` prevents the tool call. `"ask"` prompts the user to confirm. `"defer"` exits gracefully so the tool can be resumed later. [Deny and ask rules](https://code.claude.com/docs/en/permissions#manage-permissions) are still evaluated regardless of what the hook returns |
1672
+ | `permissionDecisionReason` | For `"ask"`, shown to the user in the permission prompt. When Claude Code [denies the call](https://code.claude.com/docs/en/headless#turn-off-permission-prompts-in-unattended-runs) in a `-p` run where no one can answer that prompt, Claude reads the reason in the tool result instead. For `"deny"`, shown to Claude. For `"allow"` and `"defer"`, written to the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks) only |
1673
+ | `updatedInput` | Modifies the tool’s input parameters before execution. Replaces the entire input object, so include unchanged fields alongside modified ones. Claude Code evaluates permission rules and a Bash command’s [auto-background eligibility](https://code.claude.com/docs/en/tools-reference#foreground-commands-that-move-to-the-background) against the input your hook returns, not the input Claude sent. Combine with `"allow"` to auto-approve, or `"ask"` to show the modified input to the user. For `"defer"`, ignored |
1356
1674
  | `additionalContext` | String added to Claude’s context alongside the tool result. Ignored when `permissionDecision` is `"defer"`. See [Add context for Claude](https://code.claude.com/docs/en/hooks#add-context-for-claude) |
1357
1675
 
1358
- When multiple PreToolUse hooks return different decisions, precedence is `deny` \> `defer` \> `ask` \> `allow`.When a hook returns `"ask"`, the permission prompt displayed to the user includes a label identifying where the hook came from: for example, `[User]`, `[Project]`, `[Plugin]`, or `[Local]`. This helps users understand which configuration source is requesting confirmation.
1676
+ When multiple PreToolUse hooks return different decisions, precedence is `deny` \> `defer` \> `ask` \> `allow`.A hook that blocks by exiting 2 routes the same way as `"deny"`: Claude sees the stderr message as the denial reason.When a hook returns `"ask"`, the permission prompt displayed to the user includes a label identifying where the hook came from: `[settings]` for a hook from any settings file or from agent frontmatter, `[plugin:<name>]` for a plugin’s hook, or `[skill]` for a hook from skill frontmatter. This helps users understand which configuration source is requesting confirmation.A hook’s `"ask"` also forces a permission prompt in [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode): the classifier can still deny the tool call, but it can’t approve the call silently. Before v2.1.211, the classifier could approve a Bash command running outside the [sandbox](https://code.claude.com/docs/en/sandboxing) without showing the prompt the hook requested; the classifier still applied its own safety rules to that command, and a hook `"deny"` was always honored.
1359
1677
 
1360
1678
  ```
1361
1679
  {
@@ -1371,22 +1689,48 @@ When multiple PreToolUse hooks return different decisions, precedence is `deny`
1371
1689
  }
1372
1690
  ```
1373
1691
 
1374
- `AskUserQuestion` and `ExitPlanMode` require user interaction and normally block in [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag. Returning `permissionDecision: "allow"` together with `updatedInput` satisfies that requirement: the hook reads the tool’s input from stdin, collects the answer through your own UI, and returns it in `updatedInput` so the tool runs without prompting. Returning `"allow"` alone is not sufficient for these tools. For `AskUserQuestion`, echo back the original `questions` array and add an [`answers`](https://code.claude.com/docs/en/hooks#askuserquestion) object mapping each question’s text to the chosen answer.As of v2.1.199, an MCP tool whose server marks it with [`_meta["anthropic/requiresUserInteraction"]`](https://code.claude.com/docs/en/mcp#require-approval-for-a-specific-tool) is stricter: a hook can’t skip its approval prompt with `"allow"`, with or without `updatedInput`, because Claude Code can’t confirm the hook collected the interaction the tool needs.
1375
-
1376
1692
  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.
1377
1693
 
1378
- #### [​](https://code.claude.com/docs/en/hooks\#defer-a-tool-call-for-later) Defer a tool call for later
1694
+ #### [​](https://code.claude.com/docs/en/hooks\#allow-with-updatedinput) Tools that require user interaction
1695
+
1696
+ `AskUserQuestion` and `ExitPlanMode` require user interaction. In [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag, Claude Code offers them only when the run has a [permission host](https://code.claude.com/docs/en/headless#turn-off-permission-prompts-in-unattended-runs) to receive the prompt, such as an Agent SDK `canUseTool` callback.A `PreToolUse` hook satisfies that requirement when it does the following:
1697
+
1698
+ 1. Reads the tool’s input from stdin
1699
+ 2. Collects the answer through your own UI
1700
+ 3. Returns `permissionDecision: "allow"` together with `updatedInput` that holds the answer, so the tool runs without prompting
1701
+
1702
+ Returning `"allow"` alone is not sufficient for these tools.For `AskUserQuestion`, echo back the original `questions` array and add an [`answers`](https://code.claude.com/docs/en/hooks#askuserquestion) object mapping each question’s text to the chosen answer. This output answers one question with `React`:
1703
+
1704
+ ```
1705
+ {
1706
+ "hookSpecificOutput": {
1707
+ "hookEventName": "PreToolUse",
1708
+ "permissionDecision": "allow",
1709
+ "updatedInput": {
1710
+ "questions": [\
1711
+ {\
1712
+ "question": "Which framework?",\
1713
+ "header": "Framework",\
1714
+ "options": [{"label": "React", "description": "Component library"}, {"label": "Vue", "description": "Progressive framework"}],\
1715
+ "multiSelect": false\
1716
+ }\
1717
+ ],
1718
+ "answers": {"Which framework?": "React"}
1719
+ }
1720
+ }
1721
+ }
1722
+ ```
1379
1723
 
1380
- `"defer"` is for integrations that run `claude -p` as a subprocess and read its JSON output, such as an Agent SDK app or a custom UI built on top of Claude Code. It lets that calling process pause Claude at a tool call, collect input through its own interface, and resume where it left off. Claude Code honors this value only in [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag. In interactive sessions it logs a warning and ignores the hook result.
1724
+ An MCP tool whose server marks it with [`_meta["anthropic/requiresUserInteraction"]`](https://code.claude.com/docs/en/mcp#require-approval-for-a-specific-tool) is stricter: a hook can’t skip its approval prompt with `"allow"`, with or without `updatedInput`, because Claude Code can’t confirm the hook collected the interaction the tool needs.
1381
1725
 
1382
- The `defer` value requires Claude Code v2.1.89 or later. Earlier versions don’t recognize it and the tool proceeds through the normal permission flow.
1726
+ #### [​](https://code.claude.com/docs/en/hooks\#defer-a-tool-call-for-later) Defer a tool call for later
1383
1727
 
1384
- The `AskUserQuestion` tool is the typical case: Claude wants to ask the user something, but there is no terminal to answer in. The round trip works like this:
1728
+ `"defer"` is for integrations that run `claude -p` as a subprocess and read its JSON output, such as an Agent SDK app or a custom UI built on top of Claude Code. It lets that calling process pause Claude at a tool call, collect input through its own interface, and resume where it left off. Claude Code honors this value only in [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag. In interactive sessions it logs a warning and ignores the hook result.The `AskUserQuestion` tool is the typical case: Claude wants to ask the user something, but there is no terminal to answer in. A `-p` run offers `AskUserQuestion` only when it has a [permission host](https://code.claude.com/docs/en/headless#turn-off-permission-prompts-in-unattended-runs), such as an MCP tool you pass with `--permission-prompt-tool`, so start the run with one. The round trip works like this:
1385
1729
 
1386
1730
  1. Claude calls `AskUserQuestion`. The `PreToolUse` hook fires.
1387
1731
  2. The hook returns `permissionDecision: "defer"`. The tool doesn’t execute. The process exits with `stop_reason: "tool_deferred"` and the pending tool call preserved in the transcript.
1388
1732
  3. The calling process reads `deferred_tool_use` from the SDK result, surfaces the question in its own UI, and waits for an answer.
1389
- 4. The calling process runs `claude -p --resume <session-id>`. The same tool call fires `PreToolUse` again.
1733
+ 4. The calling process runs `claude -p --resume <session-id>` with the same permission host. The same tool call fires `PreToolUse` again.
1390
1734
  5. The hook returns `permissionDecision: "allow"` with the answer in `updatedInput`. The tool executes and Claude continues.
1391
1735
 
1392
1736
  The `deferred_tool_use` field carries the tool’s `id`, `name`, and `input`. The `input` is the parameters Claude generated for the tool call, captured before execution:
@@ -1400,23 +1744,23 @@ The `deferred_tool_use` field carries the tool’s `id`, `name`, and `input`. Th
1400
1744
  "deferred_tool_use": {
1401
1745
  "id": "toolu_01abc",
1402
1746
  "name": "AskUserQuestion",
1403
- "input": { "questions": [{ "question": "Which framework?", "header": "Framework", "options": [{"label": "React"}, {"label": "Vue"}], "multiSelect": false }] }
1747
+ "input": { "questions": [{ "question": "Which framework?", "header": "Framework", "options": [{"label": "React", "description": "Component library"}, {"label": "Vue", "description": "Progressive framework"}], "multiSelect": false }] }
1404
1748
  }
1405
1749
  }
1406
1750
  ```
1407
1751
 
1408
- There is no timeout or retry limit. The session remains on disk until you resume it, subject to the [`cleanupPeriodDays`](https://code.claude.com/docs/en/settings#available-settings) retention sweep that deletes session files after 30 days by default. If the answer is not ready when you resume, the hook can return `"defer"` again and the process exits the same way. The calling process controls when to break the loop by eventually returning `"allow"` or `"deny"` from the hook.`"defer"` only works when Claude makes a single tool call in the turn. If Claude makes several tool calls at once, `"defer"` is ignored with a warning and the tool proceeds through the normal permission flow. The constraint exists because resume can only re-run one tool: there is no way to defer one call from a batch without leaving the others unresolved.If 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.
1752
+ There is no timeout or retry limit. The session remains on disk until you resume it, subject to the [`cleanupPeriodDays`](https://code.claude.com/docs/en/settings-reference#cleanupperioddays) retention sweep, which deletes session files after 30 days by default, following the [retention sweep rules](https://code.claude.com/docs/en/claude-directory#cleaned-up-automatically). If the answer is not ready when you resume, the hook can return `"defer"` again and the process exits the same way. The calling process controls when to break the loop by eventually returning `"allow"` or `"deny"` from the hook.`"defer"` only works when Claude makes a single tool call in the turn. If Claude makes several tool calls at once, `"defer"` is ignored with a warning and the tool proceeds through the normal permission flow. The constraint exists because resume can only re-run one tool: there is no way to defer one call from a batch without leaving the others unresolved.If 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.
1409
1753
 
1410
- `--resume` restores the permission mode that was active when the tool was deferred, so you don’t need to pass `--permission-mode` again. The exceptions are `plan` and `bypassPermissions`, which are never carried over. Passing `--permission-mode` explicitly on resume overrides the restored value.
1754
+ To resume a deferred session in plan mode, pass [`--permission-prompt-tool`](https://code.claude.com/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`](https://code.claude.com/docs/en/sessions#resume-in-plan-mode-with-p). Requires Claude Code v2.1.246 or later.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](https://code.claude.com/docs/en/sessions#permission-mode-on-resume).
1411
1755
 
1412
1756
  ### [​](https://code.claude.com/docs/en/hooks\#permissionrequest) PermissionRequest
1413
1757
 
1414
- Runs when the user is shown a permission dialog.
1415
- Use [PermissionRequest decision control](https://code.claude.com/docs/en/hooks#permissionrequest-decision-control) to allow or deny on behalf of the user.Matches on tool name, same values as PreToolUse.
1758
+ Runs when Claude Code is about to ask you for permission to use a tool. In sessions that can’t show a prompt, such as background subagents in [non-interactive mode](https://code.claude.com/docs/en/headless), Claude Code still runs these hooks, and if no hook returns a decision, it denies the tool call. For a call that reaches a `--permission-prompt-tool` or the Agent SDK’s [`canUseTool` callback](https://code.claude.com/docs/en/agent-sdk/permissions), the hooks run alongside your host, and whichever decides first applies.
1759
+ Use [PermissionRequest decision control](https://code.claude.com/docs/en/hooks#permissionrequest-decision-control) to allow or deny on behalf of the user.Use this event when you need a signal the moment Claude asks for permission to use a tool. Claude Code runs a [Notification](https://code.claude.com/docs/en/hooks#notification) hook with the `permission_prompt` type only after the prompt has waited about six seconds.Claude Code doesn’t run PermissionRequest hooks for a sandboxed command’s [network request](https://code.claude.com/docs/en/sandboxing#network-isolation). To get a signal for that prompt, use the `permission_prompt` notification type.Matches on tool name, same values as PreToolUse.
1416
1760
 
1417
1761
  #### [​](https://code.claude.com/docs/en/hooks\#permissionrequest-input) PermissionRequest input
1418
1762
 
1419
- PermissionRequest hooks receive `tool_name` and `tool_input` fields like PreToolUse hooks, but without `tool_use_id`. An optional `permission_suggestions` array contains the “always allow” options the user would normally see in the permission dialog. The difference is when the hook fires: PermissionRequest hooks run when a permission dialog is about to be shown to the user, while PreToolUse hooks run before tool execution regardless of permission status.
1763
+ PermissionRequest hooks receive `tool_name` and `tool_input` fields like PreToolUse hooks, but without `tool_use_id`. For an MCP tool, they also receive the [`mcp_server`](https://code.claude.com/docs/en/hooks#pretooluse-input) object. An optional `permission_suggestions` array contains the [permission updates](https://code.claude.com/docs/en/hooks#permission-update-entries) Claude Code suggests for this request, such as adding an allow rule or changing the permission mode.The `permission_suggestions` array isn’t an exact list of the options you see, because each permission dialog builds its own options. Some dialogs, such as the one for file edits, don’t read the array at all and derive their options from the request itself. A dialog that does read it can still withhold an option whose suggestion stays in the array, for example when [`allowManagedPermissionRulesOnly`](https://code.claude.com/docs/en/settings-reference#allowmanagedpermissionrulesonly) hides rule-saving options. It can also offer options that have no suggestion entry, such as [**Yes, and switch to auto mode**](https://code.claude.com/docs/en/permission-modes#switch-permission-modes), which changes the permission mode directly rather than through a permission update.PreToolUse hooks run before every tool call, whether or not it needs permission. PermissionRequest hooks run only when Claude Code is about to ask you for permission, or when it would otherwise auto-deny a call that can’t prompt. Neither event fires for [`EndConversation`](https://code.claude.com/docs/en/tools-reference#endconversation-tool-behavior).
1420
1764
 
1421
1765
  ```
1422
1766
  {
@@ -1453,6 +1797,8 @@ PermissionRequest hooks receive `tool_name` and `tool_input` fields like PreTool
1453
1797
  | `message` | For `"deny"` only: tells Claude why the permission was denied |
1454
1798
  | `interrupt` | For `"deny"` only: if `true`, stops Claude |
1455
1799
 
1800
+ A hook that exits 2 without a `decision` object leaves the permission flow unchanged, and its stderr is discarded. To grant or deny the request, return the `decision` object.
1801
+
1456
1802
  ```
1457
1803
  {
1458
1804
  "hookSpecificOutput": {
@@ -1476,11 +1822,11 @@ The `updatedPermissions` output field and the [`permission_suggestions` input fi
1476
1822
  | `addRules` | `rules`, `behavior`, `destination` | Adds permission rules. `rules` is an array of `{toolName, ruleContent?}` objects. Omit `ruleContent` to match the whole tool. `behavior` is `"allow"`, `"deny"`, or `"ask"` |
1477
1823
  | `replaceRules` | `rules`, `behavior`, `destination` | Replaces all rules of the given `behavior` at the `destination` with the provided `rules` |
1478
1824
  | `removeRules` | `rules`, `behavior`, `destination` | Removes matching rules of the given `behavior` |
1479
- | `setMode` | `mode`, `destination` | Changes the permission mode. Valid modes are `default`, `auto`, `acceptEdits`, `dontAsk`, `bypassPermissions`, `plan`, and `manual` as an alias for `default`. The `manual` alias requires Claude Code v2.1.200 or later |
1825
+ | `setMode` | `mode`, `destination` | Changes the permission mode. Valid modes are `default`, `auto`, `acceptEdits`, `dontAsk`, `bypassPermissions`, `plan`, and `manual` as an alias for `default` |
1480
1826
  | `addDirectories` | `directories`, `destination` | Adds working directories. `directories` is an array of path strings |
1481
1827
  | `removeDirectories` | `directories`, `destination` | Removes working directories |
1482
1828
 
1483
- `setMode` with `bypassPermissions` only takes effect if the session was launched with bypass mode already available: `--dangerously-skip-permissions`, `--permission-mode bypassPermissions`, `--allow-dangerously-skip-permissions`, or `permissions.defaultMode: "bypassPermissions"` in settings, and the mode is not disabled by [`permissions.disableBypassPermissionsMode`](https://code.claude.com/docs/en/permissions#managed-settings). Otherwise the update is a no-op. `bypassPermissions` is never persisted as `defaultMode` regardless of `destination`.
1829
+ `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](https://code.claude.com/docs/en/settings-reference#permissions-defaultmode). Otherwise the update is a no-op. The update is also a no-op when [`permissions.disableBypassPermissionsMode`](https://code.claude.com/docs/en/permissions#managed-settings) disables the mode, or when the session starts in [restricted mode](https://code.claude.com/docs/en/cli-reference#cli-flags).`bypassPermissions` is never persisted as `defaultMode` regardless of `destination`.
1484
1830
 
1485
1831
  The `destination` field on every entry determines whether the change stays in memory or persists to a settings file.
1486
1832
 
@@ -1491,15 +1837,18 @@ The `destination` field on every entry determines whether the change stays in me
1491
1837
  | `projectSettings` | `.claude/settings.json` |
1492
1838
  | `userSettings` | `~/.claude/settings.json` |
1493
1839
 
1494
- A hook can echo one of the `permission_suggestions` it received as its own `updatedPermissions` output, which is equivalent to the user selecting that “always allow” option in the dialog.
1840
+ A hook can echo one of the `permission_suggestions` it received as its own `updatedPermissions` output.
1495
1841
 
1496
1842
  ### [​](https://code.claude.com/docs/en/hooks\#posttooluse) PostToolUse
1497
1843
 
1498
- Runs immediately after a tool completes successfully.Matches on tool name, same values as PreToolUse.
1844
+ Runs immediately after a tool completes successfully.Matches on tool name, same values as PreToolUse.Match more broadly when the tool name isn’t the right filter:
1845
+
1846
+ - To run a hook after any tool completes successfully, omit the `matcher` or set it to `"*"`. Your hook can then discover what changed itself, for example by running `git status --porcelain`, which also lists untracked files that `git diff` misses. For tool calls that fail, add the same hook under [PostToolUseFailure](https://code.claude.com/docs/en/hooks#posttoolusefailure).
1847
+ - To run a hook when a specific file changes on disk, whatever wrote it, use [FileChanged](https://code.claude.com/docs/en/hooks#filechanged). Claude Code doesn’t run a `PostToolUse` hook matching `Edit|Write` when a `Bash` command or a process outside Claude Code rewrites the same file.
1499
1848
 
1500
1849
  #### [​](https://code.claude.com/docs/en/hooks\#posttooluse-input) PostToolUse input
1501
1850
 
1502
- `PostToolUse` hooks fire after a tool has already executed successfully. The input includes both `tool_input`, the arguments sent to the tool, and `tool_response`, the result it returned. The exact schema for both depends on the tool.
1851
+ `PostToolUse` hooks fire after a tool has already executed successfully. The input includes both `tool_input`, the arguments sent to the tool, and `tool_response`, the result it returned. The exact schema for both depends on the tool. File-tool `tool_input` paths arrive in the same format as for [PreToolUse](https://code.claude.com/docs/en/hooks#pretooluse-input): always absolute, with the platform’s native separators, so backslashes on Windows. For an MCP tool, the input also carries the [`mcp_server`](https://code.claude.com/docs/en/hooks#pretooluse-input) object.
1503
1852
 
1504
1853
  ```
1505
1854
  {
@@ -1515,7 +1864,7 @@ Runs immediately after a tool completes successfully.Matches on tool name, same
1515
1864
  },
1516
1865
  "tool_response": {
1517
1866
  "filePath": "/path/to/file.txt",
1518
- "success": true
1867
+ "type": "create"
1519
1868
  },
1520
1869
  "tool_use_id": "toolu_01ABC123...",
1521
1870
  "duration_ms": 12
@@ -1535,6 +1884,7 @@ Runs immediately after a tool completes successfully.Matches on tool name, same
1535
1884
  | `decision` | `"block"` adds the `reason` next to the tool result. Claude still sees the original output; to replace it, use `updatedToolOutput` |
1536
1885
  | `reason` | Explanation shown to Claude when `decision` is `"block"` |
1537
1886
  | `additionalContext` | String added to Claude’s context alongside the tool result. See [Add context for Claude](https://code.claude.com/docs/en/hooks#add-context-for-claude) |
1887
+ | `classifierContext` | Short note about this call’s result for the [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode) classifier rather than for Claude. See [Annotate a result for the auto mode classifier](https://code.claude.com/docs/en/hooks#annotate-a-result-for-the-auto-mode-classifier). Requires Claude Code v2.1.236 or later |
1538
1888
  | `updatedToolOutput` | Replaces the tool’s output with the provided value before it is sent to Claude. The value must match the tool’s output shape |
1539
1889
  | `updatedMCPToolOutput` | Replaces the output for [MCP tools](https://code.claude.com/docs/en/hooks#match-mcp-tools) only. Prefer `updatedToolOutput`, which works for all tools |
1540
1890
 
@@ -1557,13 +1907,42 @@ The example below replaces the output of a `Bash` call. The replacement value ma
1557
1907
 
1558
1908
  `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](https://code.claude.com/docs/en/hooks#pretooluse) hook instead.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.
1559
1909
 
1910
+ #### [​](https://code.claude.com/docs/en/hooks\#annotate-a-result-for-the-auto-mode-classifier) Annotate a result for the auto mode classifier
1911
+
1912
+ Return `classifierContext` to send a short note about the tool call’s result to the [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode) classifier rather than to Claude. The classifier [never receives tool results themselves](https://code.claude.com/docs/en/permission-modes#how-the-classifier-evaluates-actions), so this field is the supported way to tell it something about what a call returned before it reviews later actions. The field requires Claude Code v2.1.236 or later.The example below tells the classifier where a query’s output came from:
1913
+
1914
+ ```
1915
+ {
1916
+ "hookSpecificOutput": {
1917
+ "hookEventName": "PostToolUse",
1918
+ "classifierContext": "This query ran against the staging database, not production."
1919
+ }
1920
+ }
1921
+ ```
1922
+
1923
+ How much weight the classifier gives the note depends on where you configured the hook:
1924
+
1925
+ - **Hooks configured in Claude Code**: for hooks from settings files, plugins, skills, and agent frontmatter, the classifier treats the note as unverified, application-provided context. The note never establishes user intent, and if it claims you approved or requested something, the classifier checks that claim against your own messages in the conversation
1926
+ - **In-process Agent SDK callbacks**: when an application embedding Claude Code registers the hook as a [TypeScript SDK callback](https://code.claude.com/docs/en/agent-sdk/hooks) and returns the note during the live session, the classifier may weigh a user statement relayed in the note as user intent. Such a statement can satisfy a consent requirement the classifier would accept from a message you send, but it never lifts a block that your own message couldn’t lift either. After a session resumes, Claude Code treats restored notes as unverified context. When hooks from both groups annotate the same call, the classifier treats the combined note as unverified
1927
+
1928
+ Claude Code applies these limits when delivering the note:
1929
+
1930
+ - **Length**: Claude Code caps the notes for one tool call at 2,000 characters and truncates the rest. The cap is shared across every hook that responds to that call
1931
+ - **Synchronous responses only**: Claude Code ignores the field in the response of a hook that [runs in the background](https://code.claude.com/docs/en/hooks#run-hooks-in-the-background), because that response arrives after Claude Code records the tool result
1932
+ - **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
1933
+ - **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
1934
+
1935
+ 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.
1936
+
1560
1937
  ### [​](https://code.claude.com/docs/en/hooks\#posttoolusefailure) PostToolUseFailure
1561
1938
 
1562
- Runs when a tool execution fails. This event fires for tool calls that throw errors or return failure results. Use this to log failures, send alerts, or provide corrective feedback to Claude.Matches on tool name, same values as PreToolUse.
1939
+ Runs when a tool that started executing fails: the tool threw an error, or an MCP tool returned an error result. Use this to log failures, send alerts, or provide corrective feedback to Claude.Matches on tool name, same values as PreToolUse.
1940
+
1941
+ 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](https://code.claude.com/docs/en/hooks#permissiondenied).
1563
1942
 
1564
1943
  #### [​](https://code.claude.com/docs/en/hooks\#posttoolusefailure-input) PostToolUseFailure input
1565
1944
 
1566
- PostToolUseFailure hooks receive the same `tool_name` and `tool_input` fields as PostToolUse, along with error information as top-level fields:
1945
+ PostToolUseFailure hooks receive the same `tool_name` and `tool_input` fields as PostToolUse, along with error information as top-level fields. For an MCP tool, they also receive the [`mcp_server`](https://code.claude.com/docs/en/hooks#pretooluse-input) object. For example, a failed `npm test` command might deliver:
1567
1946
 
1568
1947
  ```
1569
1948
  {
@@ -1578,7 +1957,7 @@ PostToolUseFailure hooks receive the same `tool_name` and `tool_input` fields as
1578
1957
  "description": "Run test suite"
1579
1958
  },
1580
1959
  "tool_use_id": "toolu_01ABC123...",
1581
- "error": "Command exited with non-zero status code 1",
1960
+ "error": "Exit code 1\nError: Cannot find module 'express'",
1582
1961
  "is_interrupt": false,
1583
1962
  "duration_ms": 4187
1584
1963
  }
@@ -1586,10 +1965,16 @@ PostToolUseFailure hooks receive the same `tool_name` and `tool_input` fields as
1586
1965
 
1587
1966
  | Field | Description |
1588
1967
  | --- | --- |
1589
- | `error` | String describing what went wrong |
1590
- | `is_interrupt` | Optional boolean indicating whether the failure was caused by user interruption |
1968
+ | `error` | String describing what went wrong. The format depends on the tool that failed |
1969
+ | `is_interrupt` | Optional boolean. True when the failure reached Claude Code as an abort rather than as an error the tool reported. Cancelling a running tool does not fire this hook; the tool result carries the interruption message instead |
1591
1970
  | `duration_ms` | Optional. Tool execution time in milliseconds. Excludes time spent in permission prompts and PreToolUse hooks |
1592
1971
 
1972
+ The `error` string is generally the same text Claude receives as the failed tool’s result. Its format varies by tool and failure. Key your hook on `tool_name`, `is_interrupt`, and the `Exit code N` first line; treat the rest of the string as display text, not a stable format.
1973
+
1974
+ - For Bash and PowerShell, a command that ran and exited produces a first line `Exit code N`, then any output the command produced as one block with stdout and stderr interleaved
1975
+ - A payload may also carry a bare failure message with no exit-code line, when Claude Code could not start the shell process itself
1976
+ - Claude Code middle-truncates long strings around a `... [N characters truncated] ...` marker, and can insert lines of its own, such as `Command timed out after 2m 0s`
1977
+
1593
1978
  #### [​](https://code.claude.com/docs/en/hooks\#posttoolusefailure-decision-control) PostToolUseFailure decision control
1594
1979
 
1595
1980
  `PostToolUseFailure` hooks can provide context to Claude after a tool failure. In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) available to all hooks, your hook script can return these event-specific fields:
@@ -1627,13 +2012,13 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1627
2012
  "tool_name": "Read",\
1628
2013
  "tool_input": {"file_path": "/.../ledger/accounts.py"},\
1629
2014
  "tool_use_id": "toolu_01...",\
1630
- "tool_response": " 1\tfrom __future__ import annotations\n 2\t..."\
2015
+ "tool_response": "1\tfrom __future__ import annotations\n2\t..."\
1631
2016
  },\
1632
2017
  {\
1633
2018
  "tool_name": "Read",\
1634
2019
  "tool_input": {"file_path": "/.../ledger/transactions.py"},\
1635
2020
  "tool_use_id": "toolu_02...",\
1636
- "tool_response": " 1\tfrom __future__ import annotations\n 2\t..."\
2021
+ "tool_response": "1\tfrom __future__ import annotations\n2\t..."\
1637
2022
  }\
1638
2023
  ]
1639
2024
  }
@@ -1641,7 +2026,7 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1641
2026
 
1642
2027
  `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.
1643
2028
 
1644
- The `tool_response` shape differs from `PostToolUse`’s. `PostToolUse` passes the tool’s structured `Output` object, such as `{filePath: "...", success: true}` for `Write`; `PostToolBatch` passes the serialized `tool_result` content the model sees.
2029
+ 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.
1645
2030
 
1646
2031
  #### [​](https://code.claude.com/docs/en/hooks\#posttoolbatch-decision-control) PostToolBatch decision control
1647
2032
 
@@ -1660,15 +2045,15 @@ The `tool_response` shape differs from `PostToolUse`’s. `PostToolUse` passes t
1660
2045
  }
1661
2046
  ```
1662
2047
 
1663
- Returning `decision: "block"` or `continue: false` stops the agentic loop before the next model call.
2048
+ Returning `decision: "block"` or `continue: false` stops the agentic loop before the next model call. The blocking message comes from the JSON `reason` or `stopReason`, or from stderr on exit 2. You see it as a warning in the transcript, and it stays in the conversation, so Claude sees it when the conversation continues.
1664
2049
 
1665
2050
  ### [​](https://code.claude.com/docs/en/hooks\#permissiondenied) PermissionDenied
1666
2051
 
1667
- Runs when the [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode) classifier denies a tool call. This hook only fires in auto mode: it doesn’t run when you manually deny a permission dialog, when a `PreToolUse` hook blocks a call, or when a `deny` rule matches. Use it to log classifier denials, adjust configuration, or tell the model it may retry the tool call.Matches on tool name, same values as PreToolUse.
2052
+ Runs when [auto mode](https://code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode) denies a tool call, including when it denies without a classifier verdict because [a safety check separate from auto mode refused the classifier’s own request](https://code.claude.com/docs/en/errors#auto-mode-cannot-determine-the-safety-of-an-action) or its response didn’t parse. This hook only fires in auto mode: it doesn’t run when you manually deny a permission dialog, when a `PreToolUse` hook blocks a call, or when a `deny` rule matches. Use it to log denials, adjust configuration, or tell the model it may retry the tool call.Matches on tool name, same values as PreToolUse.
1668
2053
 
1669
2054
  #### [​](https://code.claude.com/docs/en/hooks\#permissiondenied-input) PermissionDenied input
1670
2055
 
1671
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PermissionDenied hooks receive `tool_name`, `tool_input`, `tool_use_id`, and `reason`.
2056
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PermissionDenied hooks receive `tool_name`, `tool_input`, `tool_use_id`, and `reason`. For an MCP tool, they also receive the [`mcp_server`](https://code.claude.com/docs/en/hooks#pretooluse-input) object.
1672
2057
 
1673
2058
  ```
1674
2059
  {
@@ -1683,13 +2068,13 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1683
2068
  "description": "Clean build directory"
1684
2069
  },
1685
2070
  "tool_use_id": "toolu_01ABC123...",
1686
- "reason": "Auto mode denied: command targets a path outside the project"
2071
+ "reason": "[Irreversible Local Destruction]"
1687
2072
  }
1688
2073
  ```
1689
2074
 
1690
2075
  | Field | Description |
1691
2076
  | --- | --- |
1692
- | `reason` | The classifier’s explanation for why the tool call was denied |
2077
+ | `reason` | The denial reason. For a classifier verdict, in most sessions it names the matched rule in square brackets, such as `[Data Exfiltration]`; see [Review denials](https://code.claude.com/docs/en/auto-mode-config#review-denials) for the other forms. For a [no-verdict denial](https://code.claude.com/docs/en/hooks#permissiondenied-decision-control), it starts with `Auto mode could not evaluate this action and is blocking it for safety`. For a denial because the classifier model was unavailable, it is the fixed text `Classifier unavailable` |
1693
2078
 
1694
2079
  #### [​](https://code.claude.com/docs/en/hooks\#permissiondenied-decision-control) PermissionDenied decision control
1695
2080
 
@@ -1704,24 +2089,44 @@ PermissionDenied hooks can tell the model it may retry the denied tool call. Ret
1704
2089
  }
1705
2090
  ```
1706
2091
 
1707
- When `retry` is `true`, Claude Code adds a message to the conversation telling the model it may retry the tool call. The denial itself is not reversed. If your hook doesn’t return JSON, or returns `retry: false`, the denial stands and the model receives the original rejection message.
2092
+ When `retry` is `true`, Claude Code adds a message to the conversation telling the model it may retry the tool call. Claude Code doesn’t reverse the denial itself. If your hook doesn’t return JSON, or returns `retry: false`, the denial stands and the model receives the original rejection message.Claude Code ignores `retry: true` when the classifier produced [no verdict on the action](https://code.claude.com/docs/en/errors#auto-mode-cannot-determine-the-safety-of-an-action): its response didn’t parse, or a safety check separate from auto mode refused the classifier’s own request. For those denials, Claude Code already tells the model in the rejection message whether to retry later or move on.
1708
2093
 
1709
2094
  ### [​](https://code.claude.com/docs/en/hooks\#notification) Notification
1710
2095
 
1711
- Runs when Claude Code sends notifications. Matches on notification type. Omit the matcher to run hooks for all notification types.
2096
+ Runs when Claude Code sends notifications. Matches on notification type. Omit the matcher to run hooks for all notification types.You receive these hook events even with desktop notifications turned off: the `preferredNotifChannel` setting, including `notifications_disabled`, changes only how you’re alerted, not whether your hook runs.
1712
2097
 
1713
2098
  | Matcher | When it fires |
1714
2099
  | --- | --- |
1715
- | `permission_prompt` | Claude needs you to approve a tool use |
1716
- | `idle_prompt` | Claude is done and waiting for your next prompt |
2100
+ | `permission_prompt` | Claude needs you to approve a tool use or a sandboxed command’s [network request](https://code.claude.com/docs/en/sandboxing#network-isolation), and the prompt has waited about six seconds |
2101
+ | `idle_prompt` | Claude finished responding about 60 seconds ago and you haven’t typed since |
1717
2102
  | `auth_success` | Authentication completes |
1718
- | `elicitation_dialog` | An MCP server opens an elicitation form |
1719
- | `elicitation_complete` | An MCP elicitation form is submitted or dismissed |
2103
+ | `elicitation_dialog` | An MCP server opens an elicitation form and you haven’t typed for about six seconds |
2104
+ | `elicitation_url_dialog` | An MCP server asks you to open a browser URL and you haven’t typed for about six seconds |
2105
+ | `elicitation_complete` | An MCP server reports that a [URL-mode elicitation](https://code.claude.com/docs/en/hooks#elicitation-input) is complete |
1720
2106
  | `elicitation_response` | An MCP elicitation response is sent back to the server |
1721
- | `agent_needs_input` | A background session starts waiting on your input. Fires only while [agent view](https://code.claude.com/docs/en/agent-view) is open in a terminal |
2107
+ | `agent_needs_input` | A background session starts waiting on your input while [agent view](https://code.claude.com/docs/en/agent-view) is open in a terminal. Also fires when a terminal session shows you an [agent team teammate’s terminal setup question](https://code.claude.com/docs/en/agent-teams#choose-a-display-mode) or auto mode’s notice about [classifier request charges](https://code.claude.com/docs/en/auto-mode-classifier-billing) and you haven’t typed for about six seconds |
1722
2108
  | `agent_completed` | A background session finishes or fails. Fires only while [agent view](https://code.claude.com/docs/en/agent-view) is open in a terminal |
2109
+ | `quota_auto_resume_fired` | Claude Code continues your task after a claude.ai usage limit paused it: at the reset, or sooner when something you do in Claude Code during the wait, such as adding usage credits, upgrading your plan, or switching models, makes usage available again, with the [model-setting exception](https://code.claude.com/docs/en/interactive-mode#wait-for-a-usage-limit-to-reset) |
2110
+ | `quota_auto_resume_stale` | A claude.ai usage limit reset while your computer slept for more than about 30 minutes. Claude Code waits for you to press `Enter` instead of continuing. After a shorter sleep it continues and fires `quota_auto_resume_fired` instead |
2111
+ | `quota_auto_resume_disabled` | Claude Code ends its wait for a claude.ai usage limit without continuing your task: [`autoContinueAtUsageLimit`](https://code.claude.com/docs/en/settings-reference#autocontinueatusagelimit) turned off or the reset moved more than 24 hours away during a wait Claude Code started on its own, the continued task kept hitting the limit, or the continuation was blocked before it reached the model. Doesn’t fire when you press `Esc` or `Ctrl+C`, or pick **Don’t continue automatically** |
1723
2112
 
1724
- The `agent_needs_input` and `agent_completed` types require Claude Code v2.1.198 or later.Use separate matchers to run different handlers depending on the notification type. This configuration triggers a permission-specific alert script when Claude needs permission approval and a different notification when Claude has been idle:
2113
+ The `quota_auto_resume_fired`, `quota_auto_resume_stale`, and `quota_auto_resume_disabled` types require Claude Code v2.1.234 or later.In terminal sessions, `permission_prompt` for a sandboxed command’s network request requires Claude Code v2.1.246 or later.`agent_needs_input` for a teammate’s terminal setup question requires Claude Code v2.1.248 or later.
2114
+
2115
+ 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:
2116
+
2117
+ - 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](https://code.claude.com/docs/en/hooks#permissionrequest) instead.
2118
+ - 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](https://code.claude.com/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.
2119
+ - 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.
2120
+
2121
+ 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.
2122
+
2123
+ Claude Code times `permission_prompt` differently in sessions where it sends permission requests to the Agent SDK’s [`canUseTool` callback](https://code.claude.com/docs/en/agent-sdk/user-input), which is how Claude Desktop and the VS Code extension host Claude Code:
2124
+
2125
+ - Expect `permission_prompt` about six seconds after Claude asks for permission. Claude Code doesn’t defer it while you type.
2126
+ - If you or a [PermissionRequest](https://code.claude.com/docs/en/hooks#permissionrequest) hook answer sooner, Claude Code doesn’t run `permission_prompt`.
2127
+ - Set [`CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS`](https://code.claude.com/docs/en/env-vars) to `1` to turn `permission_prompt` off in these sessions.
2128
+
2129
+ Before v2.1.233, `permission_prompt` didn’t fire in these sessions.Use separate matchers to run different handlers depending on the notification type. This configuration triggers a permission-specific alert script when Claude needs permission approval and a different notification when Claude has been idle:
1725
2130
 
1726
2131
  ```
1727
2132
  {
@@ -1766,11 +2171,11 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1766
2171
  }
1767
2172
  ```
1768
2173
 
1769
- Notification hooks can’t block or modify notifications. They are intended for side effects such as forwarding the notification to an external service. The [common JSON output fields](https://code.claude.com/docs/en/hooks#json-output) such as `systemMessage` apply.
2174
+ Notification hooks can’t block or modify notifications. Claude Code discards their `systemMessage` and `continue` fields but still emits [`terminalSequence`](https://code.claude.com/docs/en/hooks#emit-terminal-notifications), which is what the desktop notification example relies on. Notification hooks are intended for side effects such as forwarding the notification to an external service.
1770
2175
 
1771
2176
  ### [​](https://code.claude.com/docs/en/hooks\#subagentstart) SubagentStart
1772
2177
 
1773
- Runs when a Claude Code subagent is spawned via the Agent tool. Supports matchers to filter by agent type name. For built-in agents, this is the agent name like `general-purpose`, `Explore`, or `Plan`. For [custom subagents](https://code.claude.com/docs/en/sub-agents), this is the `name` field from the agent’s frontmatter, not the filename.For subagents shipped by a [plugin](https://code.claude.com/docs/en/plugins), the agent type is the plugin-scoped identifier such as `my-plugin:reviewer`, not the bare frontmatter name. The colon places a plugin-scoped name on the regular-expression path, so anchor the matcher with `^` and `$` for an exact match: `^my-plugin:reviewer$`.
2178
+ Runs when Claude spawns a subagent with the Agent tool, when Claude [resumes a subagent](https://code.claude.com/docs/en/sub-agents#resume-subagents), and each time an in-process [agent team](https://code.claude.com/docs/en/agent-teams) teammate handles a new message. Supports matchers to filter by agent type name. For built-in agents, this is the agent name like `general-purpose`, `Explore`, or `Plan`. For [custom subagents](https://code.claude.com/docs/en/sub-agents), this is the `name` field from the agent’s frontmatter, not the filename.For subagents shipped by a [plugin](https://code.claude.com/docs/en/plugins/overview), the agent type is the plugin-scoped identifier such as `my-plugin:reviewer`, not the bare frontmatter name. The colon places a plugin-scoped name on the regular-expression path, so anchor the matcher with `^` and `$` for an exact match: `^my-plugin:reviewer$`.
1774
2179
 
1775
2180
  #### [​](https://code.claude.com/docs/en/hooks\#subagentstart-input) SubagentStart input
1776
2181
 
@@ -1802,13 +2207,15 @@ SubagentStart hooks can’t block subagent creation, but they can inject context
1802
2207
  }
1803
2208
  ```
1804
2209
 
2210
+ When the hook runs again for the same subagent, Claude Code injects the returned context only when the subagent’s context doesn’t already hold the copy from an earlier run. The copy injected at launch stays in place, leaving the subagent’s [prompt cache](https://code.claude.com/docs/en/prompt-caching#subagents-and-the-cache) intact. After [auto-compaction](https://code.claude.com/docs/en/sub-agents#auto-compaction) discards that copy, Claude Code injects the next run’s context again.
2211
+
1805
2212
  ### [​](https://code.claude.com/docs/en/hooks\#subagentstop) SubagentStop
1806
2213
 
1807
2214
  Runs when a Claude Code subagent has finished responding. Matches on agent type, same values as SubagentStart.
1808
2215
 
1809
2216
  #### [​](https://code.claude.com/docs/en/hooks\#subagentstop-input) SubagentStop input
1810
2217
 
1811
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), SubagentStop hooks receive `stop_hook_active`, `agent_id`, `agent_type`, `agent_transcript_path`, and `last_assistant_message`. The `agent_type` field is the value used for matcher filtering. The `transcript_path` is the main session’s transcript, while `agent_transcript_path` is the subagent’s own transcript stored in a nested `subagents/` folder. The `last_assistant_message` field contains the text content of the subagent’s final response, so hooks can access it without parsing the transcript file.SubagentStop hooks also receive the `background_tasks` and `session_crons` arrays described under [Stop input](https://code.claude.com/docs/en/hooks#stop-input), available in Claude Code v2.1.145 or later. Both arrays are scoped to the parent session, not the subagent.
2218
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), SubagentStop hooks receive `stop_hook_active`, `agent_id`, `agent_type`, `agent_transcript_path`, and `last_assistant_message`. The `agent_type` field is the value used for matcher filtering. The `transcript_path` is the main session’s transcript, while `agent_transcript_path` is the subagent’s own transcript stored in a nested `subagents/` folder. The `last_assistant_message` field contains the text content of the subagent’s final response, so hooks can access it without parsing the transcript file.Not every SubagentStop event comes from a subagent Claude spawned. Claude Code also runs internal agents for some of its own features, such as [prompt suggestions](https://code.claude.com/docs/en/interactive-mode#prompt-suggestions) and [`/btw` side questions](https://code.claude.com/docs/en/interactive-mode#side-questions-with-%2Fbtw), and SubagentStop fires when one of those finishes too. For those events, `agent_type` is the agent name the session itself runs as, such as one set with [`--agent`](https://code.claude.com/docs/en/cli-reference#cli-flags) or the [`agent` setting](https://code.claude.com/docs/en/settings-reference#agent), and an empty string when the session runs without one.A `matcher` that names agent types doesn’t match an empty `agent_type`. A hook whose matcher is omitted, `""`, or `"*"`, or is a regular expression that matches an empty string, runs for events with an empty `agent_type` too.On Claude Code v2.1.271 or later, a subagent that runs with the [`SubagentHandback`](https://code.claude.com/docs/en/tools-reference) tool delivers its report through that tool before it stops. The `last_assistant_message` field then holds the subagent’s closing text, if any, which is not the delivered report. The report is that call’s `message` input, which a `PreToolUse` or `PostToolUse` hook matched on `SubagentHandback` receives as `tool_input.message`.SubagentStop hooks also receive the `background_tasks` and `session_crons` arrays described under [Stop input](https://code.claude.com/docs/en/hooks#stop-input). Both arrays are scoped to the parent session, not the subagent.
1812
2219
 
1813
2220
  ```
1814
2221
  {
@@ -1827,11 +2234,11 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1827
2234
  }
1828
2235
  ```
1829
2236
 
1830
- SubagentStop hooks use the same decision control format as [Stop hooks](https://code.claude.com/docs/en/hooks#stop-decision-control), including `hookSpecificOutput.additionalContext` with `hookEventName` set to `"SubagentStop"`, for non-error feedback that keeps the subagent running. Returning `decision: "block"` with a `reason` keeps the subagent running and delivers `reason` to the subagent as its next instruction. To inject context into the parent session after a subagent returns, use a [`PostToolUse`](https://code.claude.com/docs/en/hooks#posttooluse) hook on the `Agent` tool instead.
2237
+ SubagentStop hooks use the same decision control format as [Stop hooks](https://code.claude.com/docs/en/hooks#stop-decision-control), including `hookSpecificOutput.additionalContext` with `hookEventName` set to `"SubagentStop"`, for non-error feedback that keeps the subagent running. Returning `decision: "block"` with a `reason` keeps the subagent running and delivers `reason` to the subagent as its next instruction. A hook that blocks by exiting 2 delivers its stderr message the same way. To inject context into the parent session after a subagent returns, use a [`PostToolUse`](https://code.claude.com/docs/en/hooks#posttooluse) hook on the `Agent` tool instead.
1831
2238
 
1832
2239
  ### [​](https://code.claude.com/docs/en/hooks\#taskcreated) TaskCreated
1833
2240
 
1834
- Runs when a task is being created via the `TaskCreate` tool. Use this to enforce naming conventions, require task descriptions, or prevent certain tasks from being created.When a `TaskCreated` hook exits with code 2, the task is not created and the stderr message is fed back to the model as feedback. To stop the teammate entirely instead of re-running it, return JSON with `{"continue": false, "stopReason": "..."}`. TaskCreated hooks don’t support matchers and fire on every occurrence.
2241
+ Runs when a task is being created via the `TaskCreate` tool. Use this to enforce naming conventions, require task descriptions, or prevent certain tasks from being created. In a [session without the Task tools](https://code.claude.com/docs/en/tools-reference#task-tool-availability), this event doesn’t fire.TaskCreated hooks don’t support matchers and fire on every occurrence.
1835
2242
 
1836
2243
  #### [​](https://code.claude.com/docs/en/hooks\#taskcreated-input) TaskCreated input
1837
2244
 
@@ -1842,7 +2249,6 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1842
2249
  "session_id": "abc123",
1843
2250
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1844
2251
  "cwd": "/Users/...",
1845
- "permission_mode": "default",
1846
2252
  "hook_event_name": "TaskCreated",
1847
2253
  "task_id": "task-001",
1848
2254
  "task_subject": "Implement user authentication",
@@ -1859,13 +2265,14 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1859
2265
  | `task_description` | Detailed description of the task. May be absent |
1860
2266
  | `teammate_name` | Name of the teammate creating the task. May be absent |
1861
2267
  | `team_name` | Deprecated. Session-derived team name; will be removed in a future release |
2268
+ | `agent_id` | On this event, the [common input field](https://code.claude.com/docs/en/hooks#common-input-fields) identifies the subagent or [in-process teammate](https://code.claude.com/docs/en/agent-teams#choose-a-display-mode) creating the task. May be absent. Requires Claude Code v2.1.290 or later |
1862
2269
 
1863
2270
  #### [​](https://code.claude.com/docs/en/hooks\#taskcreated-decision-control) TaskCreated decision control
1864
2271
 
1865
- TaskCreated hooks support two ways to control task creation:
2272
+ A TaskCreated hook can block the creation with exit code 2 or with a JSON decision. Either way, Claude Code deletes the task and returns your message to Claude as the tool’s error. Claude Code ignores `continue: false` from this event and Claude keeps working.
1866
2273
 
1867
- - **Exit code 2**: the task is not created and the stderr message is fed back to the model as feedback.
1868
- - **JSON `{"continue": false, "stopReason": "..."}`**: stops the teammate entirely, matching `Stop` hook behavior. The `stopReason` is shown to the user.
2274
+ - **Exit code 2**: Claude Code returns the stderr text as the message.
2275
+ - **JSON `{"decision": "block", "reason": "..."}`**: Claude Code returns `reason` as the message.
1869
2276
 
1870
2277
  This example blocks tasks whose subjects don’t follow the required format:
1871
2278
 
@@ -1884,7 +2291,7 @@ exit 0
1884
2291
 
1885
2292
  ### [​](https://code.claude.com/docs/en/hooks\#taskcompleted) TaskCompleted
1886
2293
 
1887
- Runs when a task is being marked as completed. This fires in two situations: when any agent explicitly marks a task as completed through the TaskUpdate tool, or when an [agent team](https://code.claude.com/docs/en/agent-teams) teammate finishes its turn with in-progress tasks. Use this to enforce completion criteria like passing tests or lint checks before a task can close.When a `TaskCompleted` hook exits with code 2, the task is not marked as completed and the stderr message is fed back to the model as feedback. To stop the teammate entirely instead of re-running it, return JSON with `{"continue": false, "stopReason": "..."}`. TaskCompleted hooks don’t support matchers and fire on every occurrence.
2294
+ Runs when a task is being marked as completed. This fires in two situations: when any agent explicitly marks a task as completed through the TaskUpdate tool, or when an [agent team](https://code.claude.com/docs/en/agent-teams) teammate finishes its turn with in-progress tasks. Use this to enforce completion criteria like passing tests or lint checks before a task can close.TaskCompleted hooks don’t support matchers and fire on every occurrence.
1888
2295
 
1889
2296
  #### [​](https://code.claude.com/docs/en/hooks\#taskcompleted-input) TaskCompleted input
1890
2297
 
@@ -1912,13 +2319,14 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
1912
2319
  | `task_description` | Detailed description of the task. May be absent |
1913
2320
  | `teammate_name` | Name of the teammate completing the task. May be absent |
1914
2321
  | `team_name` | Deprecated. Session-derived team name; will be removed in a future release |
2322
+ | `agent_id` | On this event, the [common input field](https://code.claude.com/docs/en/hooks#common-input-fields) identifies the subagent or [in-process teammate](https://code.claude.com/docs/en/agent-teams#choose-a-display-mode) completing the task. May be absent. Requires Claude Code v2.1.290 or later |
1915
2323
 
1916
2324
  #### [​](https://code.claude.com/docs/en/hooks\#taskcompleted-decision-control) TaskCompleted decision control
1917
2325
 
1918
2326
  TaskCompleted hooks support two ways to control task completion:
1919
2327
 
1920
2328
  - **Exit code 2**: the task is not marked as completed and the stderr message is fed back to the model as feedback.
1921
- - **JSON `{"continue": false, "stopReason": "..."}`**: stops the teammate entirely, matching `Stop` hook behavior. The `stopReason` is shown to the user.
2329
+ - **JSON `{"continue": false, "stopReason": "..."}`**: when a teammate finishing its turn triggered the event, stops the teammate entirely, matching `Stop` hook behavior. The `stopReason` is shown to the user. When the `TaskUpdate` tool triggered the event, Claude Code ignores `continue: false`; exit code 2 still blocks the completion.
1922
2330
 
1923
2331
  This example runs tests and blocks task completion if they fail:
1924
2332
 
@@ -1942,11 +2350,11 @@ Runs when the main Claude Code agent has finished responding. Does not run if
1942
2350
  the stoppage occurred due to a user interrupt. API errors fire
1943
2351
  [StopFailure](https://code.claude.com/docs/en/hooks#stopfailure) instead.
1944
2352
 
1945
- The [`/goal`](https://code.claude.com/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 until a condition holds without writing hook configuration.
2353
+ The [`/goal`](https://code.claude.com/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.
1946
2354
 
1947
2355
  #### [​](https://code.claude.com/docs/en/hooks\#stop-input) Stop input
1948
2356
 
1949
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), Stop hooks receive `stop_hook_active`, `last_assistant_message`, `background_tasks`, and `session_crons`. The `stop_hook_active` field is `true` when Claude Code is already continuing as a result of a stop hook. Check this value or process the transcript to avoid blocking on a condition that will never resolve. Claude Code overrides the hook and ends the turn after 8 consecutive blocks.The `last_assistant_message` field contains the text content of Claude’s final response, so hooks can access it without parsing the transcript file. For hooks that act on the just-completed turn, such as read-aloud or notification hooks, use this field rather than reading `transcript_path`: the transcript file isn’t guaranteed to include the final message at Stop time on all versions.The `background_tasks` and `session_crons` arrays, available in Claude Code v2.1.145 or later, let hooks distinguish “session is done” from “session is paused waiting for background work to wake it back up”. Both arrays are present when the task registry is reachable and are empty when nothing is in flight or scheduled.Each entry in `background_tasks` describes one in-flight task and uses these fields:
2357
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), Stop hooks receive `stop_hook_active`, `last_assistant_message`, `background_tasks`, and `session_crons`. The `stop_hook_active` field is `true` when Claude Code is already continuing as a result of a stop hook. Check this value or process the transcript to avoid blocking on a condition that will never resolve.Claude Code applies an 8-consecutive-continuation cap: after stop hooks have continued the turn eight times in a row, Claude Code overrides the next block and ends the turn. The count of consecutive continuations resets each time Claude calls a tool. To raise the cap, set [`CLAUDE_CODE_STOP_HOOK_BLOCK_CAP`](https://code.claude.com/docs/en/env-vars).The `last_assistant_message` field contains the text content of Claude’s final response, so hooks can access it without parsing the transcript file. For hooks that act on the just-completed turn, such as read-aloud or notification hooks, use this field rather than reading `transcript_path`: the transcript file isn’t guaranteed to include the final message at Stop time on all versions.The `background_tasks` and `session_crons` arrays let hooks distinguish “session is done” from “session is paused waiting for background work to wake it back up”. Both arrays are present when the task registry is reachable and are empty when nothing is in flight or scheduled.Each entry in `background_tasks` describes one in-flight task and uses these fields:
1950
2358
 
1951
2359
  | Field | Description |
1952
2360
  | --- | --- |
@@ -2010,6 +2418,8 @@ This example shows a Stop input with one in-flight shell task and one recurring
2010
2418
  | `reason` | Required when `decision` is `"block"`. Tells Claude why it should continue |
2011
2419
  | `hookSpecificOutput.additionalContext` | Non-error feedback for Claude. The conversation continues so Claude can act on it, but unlike `decision: "block"` it is shown in the transcript as hook feedback rather than a hook error |
2012
2420
 
2421
+ A hook that blocks by exiting 2 routes the same way as `reason`: Claude receives the stderr message as the explanation for why it should continue.
2422
+
2013
2423
  ```
2014
2424
  {
2015
2425
  "decision": "block",
@@ -2030,7 +2440,7 @@ Use `additionalContext` when the hook is working as designed and giving Claude g
2030
2440
 
2031
2441
  ### [​](https://code.claude.com/docs/en/hooks\#stopfailure) StopFailure
2032
2442
 
2033
- Runs instead of [Stop](https://code.claude.com/docs/en/hooks#stop) when the turn ends due to an API error. Output and exit code are ignored. Use this to log failures, send alerts, or take recovery actions when Claude can’t complete a response due to rate limits, authentication problems, or other API errors.
2443
+ Runs instead of [Stop](https://code.claude.com/docs/en/hooks#stop) when the turn ends due to an API error. Claude Code ignores the hook’s output and exit code, apart from [`terminalSequence`](https://code.claude.com/docs/en/hooks#emit-terminal-notifications). Use this to log failures, send alerts, or take recovery actions when Claude can’t complete a response due to rate limits, authentication problems, or other API errors.
2034
2444
 
2035
2445
  #### [​](https://code.claude.com/docs/en/hooks\#stopfailure-input) StopFailure input
2036
2446
 
@@ -2038,7 +2448,7 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2038
2448
 
2039
2449
  | Field | Description |
2040
2450
  | --- | --- |
2041
- | `error` | Error type: `rate_limit`, `overloaded`, `authentication_failed`, `oauth_org_not_allowed`, `billing_error`, `invalid_request`, `model_not_found`, `server_error`, `max_output_tokens`, or `unknown` |
2451
+ | `error` | Error type: `rate_limit`, `overloaded`, `authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `billing_error`, `invalid_request`, `model_not_found`, `server_error`, `max_output_tokens`, `cloud_credential_error`, or `unknown` |
2042
2452
  | `error_details` | Additional details about the error, when available |
2043
2453
  | `last_assistant_message` | The rendered error text shown in the conversation. Unlike `Stop` and `SubagentStop`, where this field holds Claude’s conversational output, for `StopFailure` it contains the API error string itself, such as `"API Error: Rate limit reached"` |
2044
2454
 
@@ -2058,7 +2468,7 @@ StopFailure hooks have no decision control. They run for notification and loggin
2058
2468
 
2059
2469
  ### [​](https://code.claude.com/docs/en/hooks\#teammateidle) TeammateIdle
2060
2470
 
2061
- Runs when an [agent team](https://code.claude.com/docs/en/agent-teams) teammate is about to go idle after finishing its turn. Use this to enforce quality gates before a teammate stops working, such as requiring passing lint checks or verifying that output files exist.When a `TeammateIdle` hook exits with code 2, the teammate receives the stderr message as feedback and continues working instead of going idle. To stop the teammate entirely instead of re-running it, return JSON with `{"continue": false, "stopReason": "..."}`. TeammateIdle hooks don’t support matchers and fire on every occurrence.
2471
+ Runs when an [agent team](https://code.claude.com/docs/en/agent-teams) teammate is about to go idle after finishing its turn. Use this to enforce quality gates before a teammate stops working, such as requiring passing lint checks or verifying that output files exist.TeammateIdle hooks don’t support matchers and fire on every occurrence.
2062
2472
 
2063
2473
  #### [​](https://code.claude.com/docs/en/hooks\#teammateidle-input) TeammateIdle input
2064
2474
 
@@ -2080,6 +2490,7 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2080
2490
  | --- | --- |
2081
2491
  | `teammate_name` | Name of the teammate that is about to go idle |
2082
2492
  | `team_name` | Deprecated. Session-derived team name; will be removed in a future release |
2493
+ | `agent_id` | On this event, the [common input field](https://code.claude.com/docs/en/hooks#common-input-fields) identifies the [in-process teammate](https://code.claude.com/docs/en/agent-teams#choose-a-display-mode) that is about to go idle. May be absent. Requires Claude Code v2.1.290 or later |
2083
2494
 
2084
2495
  #### [​](https://code.claude.com/docs/en/hooks\#teammateidle-decision-control) TeammateIdle decision control
2085
2496
 
@@ -2103,14 +2514,14 @@ exit 0
2103
2514
 
2104
2515
  ### [​](https://code.claude.com/docs/en/hooks\#configchange) ConfigChange
2105
2516
 
2106
- Runs when a configuration file changes during a session. Use this to audit settings changes, enforce security policies, or block unauthorized modifications to configuration files.ConfigChange hooks fire for changes to settings files, managed policy settings, and skill files. The `source` field in the input tells you which type of configuration changed, and the optional `file_path` field provides the path to the changed file.The matcher filters on the configuration source:
2517
+ Runs when a configuration file changes during a session. Use this to audit settings changes, enforce security policies, or block unauthorized modifications to configuration files.Claude Code runs ConfigChange hooks when a settings file, a managed policy file, or a skill file changes. For managed policy, it runs them only when `managed-settings.json` or a file in `managed-settings.d/` changes. It applies [server-managed settings](https://code.claude.com/docs/en/server-managed-settings) and changes to macOS managed preferences or Windows registry policy without running them. On WSL with [`wslInheritsWindowsSettings`](https://code.claude.com/docs/en/settings-reference#wslinheritswindowssettings), it also applies a changed Windows-side managed settings file on its policy poll without running them.The matcher filters on the configuration source:
2107
2518
 
2108
2519
  | Matcher | When it fires |
2109
2520
  | --- | --- |
2110
2521
  | `user_settings` | `~/.claude/settings.json` changes |
2111
2522
  | `project_settings` | `.claude/settings.json` changes |
2112
2523
  | `local_settings` | `.claude/settings.local.json` changes |
2113
- | `policy_settings` | Managed policy settings change |
2524
+ | `policy_settings` | `managed-settings.json` or a file in `managed-settings.d/` changes |
2114
2525
  | `skills` | A skill file in `.claude/skills/` changes |
2115
2526
 
2116
2527
  This example logs all configuration changes for security auditing:
@@ -2155,7 +2566,7 @@ ConfigChange hooks can block configuration changes from taking effect. Use exit
2155
2566
  | Field | Description |
2156
2567
  | --- | --- |
2157
2568
  | `decision` | `"block"` prevents the configuration change from being applied. Omit to allow the change |
2158
- | `reason` | Explanation shown to the user when `decision` is `"block"` |
2569
+ | `reason` | Accepted but never shown |
2159
2570
 
2160
2571
  ```
2161
2572
  {
@@ -2164,11 +2575,11 @@ ConfigChange hooks can block configuration changes from taking effect. Use exit
2164
2575
  }
2165
2576
  ```
2166
2577
 
2167
- `policy_settings` changes can’t be blocked. Hooks still fire for `policy_settings` sources, so you can use them for audit logging, but any blocking decision is ignored. This ensures enterprise-managed settings always take effect.
2578
+ `policy_settings` changes can’t be blocked. Hooks still fire for `policy_settings` sources when a managed settings file on the machine changes, so you can use them to log those edits, but any blocking decision is ignored. This ensures enterprise-managed settings always take effect. Claude Code doesn’t run `ConfigChange` hooks when [server-managed settings](https://code.claude.com/docs/en/server-managed-settings) arrive or refresh.Claude Code acts on the blocking decision from a ConfigChange hook’s JSON output and discards `systemMessage` and `continue`. A blocked change surfaces no message to you or to Claude, whether you block with `reason` or with stderr on exit 2. Claude Code only writes a line to the debug log.
2168
2579
 
2169
2580
  ### [​](https://code.claude.com/docs/en/hooks\#cwdchanged) CwdChanged
2170
2581
 
2171
- Runs when the working directory changes during a session, for example when Claude executes a `cd` command. Use this to react to directory changes: reload environment variables, activate project-specific toolchains, or run setup scripts automatically. Pairs with [FileChanged](https://code.claude.com/docs/en/hooks#filechanged) for tools like [direnv](https://direnv.net/) that manage per-directory environment.CwdChanged hooks have access to `CLAUDE_ENV_FILE`. Variables written to that file persist into subsequent Bash commands for the session, just as in [SessionStart hooks](https://code.claude.com/docs/en/hooks#persist-environment-variables).CwdChanged doesn’t support matchers and fires on every directory change.
2582
+ Runs when a shell command in the main conversation changes the working directory, for example when Claude executes a `cd` command. Use this to react to directory changes: reload environment variables, activate project-specific toolchains, or run setup scripts automatically. Pairs with [FileChanged](https://code.claude.com/docs/en/hooks#filechanged) for tools like [direnv](https://direnv.net/) that manage per-directory environment.CwdChanged hooks have access to [`CLAUDE_ENV_FILE`](https://code.claude.com/docs/en/hooks#persist-environment-variables). Variables written to that file persist into subsequent Bash commands until the next CwdChanged event, when Claude Code clears them.CwdChanged doesn’t support matchers and fires on every occurrence.
2172
2583
 
2173
2584
  #### [​](https://code.claude.com/docs/en/hooks\#cwdchanged-input) CwdChanged input
2174
2585
 
@@ -2193,34 +2604,104 @@ In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#js
2193
2604
  | --- | --- |
2194
2605
  | `watchPaths` | Array of absolute paths. Replaces the current dynamic watch list. Paths from your `matcher` configuration are always watched. Returning an empty array clears the dynamic list, which is typical when entering a new directory |
2195
2606
 
2196
- CwdChanged hooks have no decision control. They can’t block the directory change.
2607
+ CwdChanged hooks have no decision control. They can’t block the directory change.Claude Code reads `watchPaths` and `systemMessage` from their JSON output and discards `continue`. In interactive sessions, it shows the `systemMessage` as a brief terminal notification. The message doesn’t reach the SDK message stream.
2197
2608
 
2198
- ### [​](https://code.claude.com/docs/en/hooks\#filechanged) FileChanged
2609
+ ### [​](https://code.claude.com/docs/en/hooks\#directoryadded) DirectoryAdded
2199
2610
 
2200
- Runs when a watched file changes on disk. Useful for reloading environment variables when project configuration files are modified.The `matcher` for this event serves two roles:
2611
+ Runs after you add a working directory mid-session with the `/add-dir` command, or after an SDK client adds one with the `register_repo_root` control request. Use this to prepare a newly added repository, for example by installing its dependencies.Claude Code doesn’t fire this event when:
2201
2612
 
2202
- - **Build the watch list**: the value is split on `|` and each segment is registered as a literal filename in the working directory, so `".envrc|.env"` watches exactly those two files. Regex patterns are not useful here: a value like `^\.env` would watch a file literally named `^\.env`.
2203
- - **Filter which hooks run**: when a watched file changes, the same value filters which hook groups run using the standard [matcher rules](https://code.claude.com/docs/en/hooks#matcher-patterns) against the changed file’s basename.
2613
+ - You pass a directory with the `--add-dir` startup flag; [SessionStart](https://code.claude.com/docs/en/hooks#sessionstart) covers those directories
2614
+ - You add a directory on the `/permissions` Workspace tab
2615
+ - You add a directory that is already a working directory or inside one
2204
2616
 
2205
- FileChanged hooks have access to `CLAUDE_ENV_FILE`. Variables written to that file persist into subsequent Bash commands for the session, just as in [SessionStart hooks](https://code.claude.com/docs/en/hooks#persist-environment-variables).
2617
+ Claude Code fires DirectoryAdded after refreshing sandbox and permission state, so sandboxed tools already see the new directory when your hook runs. Hook commands themselves run unsandboxed.Claude Code doesn’t wait for the hook: the add completes immediately, and the hook runs in the background with the 600-second default timeout.The matcher filters on how the directory was added:
2206
2618
 
2207
- #### [​](https://code.claude.com/docs/en/hooks\#filechanged-input) FileChanged input
2619
+ | Matcher | When it fires |
2620
+ | --- | --- |
2621
+ | `slash_command` | You add a directory with `/add-dir` |
2622
+ | `register_repo_root` | An SDK client adds a directory with the `register_repo_root` control request |
2208
2623
 
2209
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), FileChanged hooks receive `file_path` and `event`.
2624
+ #### [​](https://code.claude.com/docs/en/hooks\#directoryadded-input) DirectoryAdded input
2625
+
2626
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), DirectoryAdded hooks receive `directory` and `source`.
2210
2627
 
2211
2628
  | Field | Description |
2212
2629
  | --- | --- |
2213
- | `file_path` | Absolute path to the file that changed |
2214
- | `event` | What happened: `"change"` for a modified file, `"add"` for a created file, or `"unlink"` for a deleted file |
2630
+ | `directory` | Absolute path of the directory that was added |
2631
+ | `source` | How the directory was added, `"slash_command"` for `/add-dir` or `"register_repo_root"` for the SDK control request |
2215
2632
 
2216
2633
  ```
2217
2634
  {
2218
2635
  "session_id": "abc123",
2219
2636
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
2220
2637
  "cwd": "/Users/my-project",
2221
- "hook_event_name": "FileChanged",
2222
- "file_path": "/Users/my-project/.envrc",
2223
- "event": "change"
2638
+ "hook_event_name": "DirectoryAdded",
2639
+ "directory": "/Users/my-other-repo",
2640
+ "source": "slash_command"
2641
+ }
2642
+ ```
2643
+
2644
+ DirectoryAdded hooks have no decision control. They can’t block the add, which has already completed when the hook runs. Claude Code discards the `continue` field from their JSON output and surfaces the rest differently per source:
2645
+
2646
+ - `slash_command`: Claude Code delivers the hook’s `systemMessage` to Claude as context on the next conversation turn, rather than showing it to you. A count of failed hooks appears in the transcript. Full failure output goes to the debug log
2647
+ - `register_repo_root`: Claude Code writes `systemMessage` output and failure output to the debug log only
2648
+
2649
+ ### [​](https://code.claude.com/docs/en/hooks\#filechanged) FileChanged
2650
+
2651
+ Runs when a watched file changes on disk. Claude Code detects changes with a filesystem watcher, not by inspecting tool calls, so it runs the hook no matter what changed the file: an `Edit` or `Write` tool call, a script Claude runs with `Bash`, or a process outside Claude Code entirely. A common use is reloading environment variables when project configuration files change.The `matcher` for this event serves two roles:
2652
+
2653
+ - **Build the watch list**: the value is split on `|` and each segment is registered as a literal filename in the working directory, so `".envrc|.env"` watches exactly those two files. Regex patterns are not useful here: a value like `^\.env` would watch a file literally named `^\.env`.
2654
+ - **Filter which hooks run**: when a watched file changes, the same value filters which hook groups run using the standard [matcher rules](https://code.claude.com/docs/en/hooks#matcher-patterns) against the changed file’s basename.
2655
+
2656
+ This example normalizes line endings in `data.csv` after any change, including a `Bash` command or an external script rewriting the file:
2657
+
2658
+ ```
2659
+ {
2660
+ "hooks": {
2661
+ "FileChanged": [\
2662
+ {\
2663
+ "matcher": "data.csv",\
2664
+ "hooks": [\
2665
+ {\
2666
+ "type": "command",\
2667
+ "command": "/path/to/normalize-line-endings.sh"\
2668
+ }\
2669
+ ]\
2670
+ }\
2671
+ ]
2672
+ }
2673
+ }
2674
+ ```
2675
+
2676
+ The hook reads the changed file’s absolute path from the `file_path` field of the [JSON input](https://code.claude.com/docs/en/hooks#filechanged-input) on stdin. Its `grep` guard tests for the same thing `perl` removes, a CR at the end of a line, so the run after a normalization exits without touching the file. A looser guard loops forever, because `perl -i` rewrites the file even when it substitutes nothing and Claude Code runs the hook again after every rewrite. Save this script at `/path/to/normalize-line-endings.sh` and make it executable:
2677
+
2678
+ ```
2679
+ #!/bin/bash
2680
+ FILE=$(jq -r .file_path)
2681
+ if grep -q $'\r$' "$FILE"; then
2682
+ perl -pi -e 's/\r$//' "$FILE"
2683
+ fi
2684
+ ```
2685
+
2686
+ To confirm the hook works, ask Claude to append a CRLF line to `data.csv` with a `Bash` command. Claude Code runs the hook and the file ends up with LF endings.To watch files you can’t name up front, return [`watchPaths`](https://code.claude.com/docs/en/hooks#filechanged-output) from a hook to update the watch list dynamically. Claude Code starts the watcher only when something names a file to watch, so seed the list with a FileChanged group whose matcher names at least one file, or with a [SessionStart](https://code.claude.com/docs/en/hooks#sessionstart-decision-control) or [CwdChanged](https://code.claude.com/docs/en/hooks#cwdchanged) hook that returns `watchPaths`. The matcher still filters which hook groups run when a watched file changes, so give the group that handles dynamic paths an omitted matcher, which matches every watched file and adds nothing to the watch list. A `"*"` matcher also matches every file, but Claude Code registers it in the watch list like any other value, as a literal file named `*`.FileChanged hooks have access to [`CLAUDE_ENV_FILE`](https://code.claude.com/docs/en/hooks#persist-environment-variables). Variables written to that file persist into subsequent Bash commands until the next [CwdChanged](https://code.claude.com/docs/en/hooks#cwdchanged) event, when Claude Code clears them.
2687
+
2688
+ #### [​](https://code.claude.com/docs/en/hooks\#filechanged-input) FileChanged input
2689
+
2690
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), FileChanged hooks receive `file_path` and `event`.
2691
+
2692
+ | Field | Description |
2693
+ | --- | --- |
2694
+ | `file_path` | Absolute path to the file that changed |
2695
+ | `event` | What happened: `"change"` for a modified file, `"add"` for a created file, or `"unlink"` for a deleted file |
2696
+
2697
+ ```
2698
+ {
2699
+ "session_id": "abc123",
2700
+ "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
2701
+ "cwd": "/Users/my-project",
2702
+ "hook_event_name": "FileChanged",
2703
+ "file_path": "/Users/my-project/.envrc",
2704
+ "event": "change"
2224
2705
  }
2225
2706
  ```
2226
2707
 
@@ -2232,11 +2713,20 @@ In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#js
2232
2713
  | --- | --- |
2233
2714
  | `watchPaths` | Array of absolute paths. Replaces the current dynamic watch list. Paths from your `matcher` configuration are always watched. Use this when your hook script discovers additional files to watch based on the changed file |
2234
2715
 
2235
- FileChanged hooks have no decision control. They can’t block the file change from occurring.
2716
+ FileChanged hooks have no decision control. They can’t block the file change from occurring.Claude Code reads `watchPaths` and `systemMessage` from their JSON output and discards `continue`. In interactive sessions, it shows the `systemMessage` as a brief terminal notification. The message doesn’t reach the SDK message stream.
2236
2717
 
2237
2718
  ### [​](https://code.claude.com/docs/en/hooks\#worktreecreate) WorktreeCreate
2238
2719
 
2239
- Runs when a worktree is being created, either from `claude --worktree` or from a [subagent using `isolation: "worktree"`](https://code.claude.com/docs/en/sub-agents#choose-the-subagent-scope). By default Claude Code creates the isolated working copy with `git worktree`. Configuring a WorktreeCreate hook replaces that default git behavior, letting you use a different version control system like SVN, Perforce, or Mercurial.Because the hook replaces the default behavior entirely, [`.worktreeinclude`](https://code.claude.com/docs/en/worktrees#copy-gitignored-files-into-worktrees) is not processed. If you need to copy local configuration files like `.env` into the new worktree, do it inside your hook script.The hook must return the path to the created worktree directory. Claude Code uses this path as the working directory for the isolated session. See [WorktreeCreate output](https://code.claude.com/docs/en/hooks#worktreecreate-output) for how each hook type returns the path.This example creates an SVN working copy and prints the path for Claude Code to use. Replace the repository URL with your own:
2720
+ Runs when Claude Code creates a worktree in cases such as these:
2721
+
2722
+ - You start a session with `claude --worktree`
2723
+ - Claude creates a worktree during a session and switches into it with the [`EnterWorktree`](https://code.claude.com/docs/en/tools-reference) tool
2724
+ - A [subagent uses `isolation: "worktree"`](https://code.claude.com/docs/en/sub-agents#choose-the-subagent-scope)
2725
+ - A [workflow](https://code.claude.com/docs/en/workflows#migrate-many-files-in-parallel) runs an agent in its own worktree
2726
+ - Claude Code isolates a [background session](https://code.claude.com/docs/en/agent-view#how-file-edits-are-isolated) in its own worktree
2727
+ - [`claude remote-control`](https://code.claude.com/docs/en/remote-control#start-a-remote-control-session) in `worktree` mode starts a session in its own worktree
2728
+
2729
+ When Claude runs `git worktree add` through the Bash tool rather than calling `EnterWorktree`, your WorktreeCreate hook doesn’t run. To inspect or block that command, add a [`PreToolUse`](https://code.claude.com/docs/en/hooks#pretooluse) hook with the `Bash` matcher.By default Claude Code creates the isolated working copy with `git worktree`. Configuring a WorktreeCreate hook replaces that default git behavior, letting you use a different version control system like SVN, Perforce, or Mercurial.Because the hook replaces the default behavior entirely, [`.worktreeinclude`](https://code.claude.com/docs/en/worktrees#copy-gitignored-files-into-worktrees) is not processed. If you need to copy local configuration files like `.env` into the new worktree, do it inside your hook script.The hook must return the path to the created worktree directory. Claude Code uses this path as the working directory for the isolated session. See [WorktreeCreate output](https://code.claude.com/docs/en/hooks#worktreecreate-output) for how each hook type returns the path.Claude Code acts on the hook’s success and the returned path, and discards `systemMessage` and `continue`.This example creates an SVN working copy and prints the path for Claude Code to use. Replace the repository URL with your own:
2240
2730
 
2241
2731
  ```
2242
2732
  {
@@ -2278,11 +2768,29 @@ WorktreeCreate hooks don’t use the standard allow/block decision model. Instea
2278
2768
  - **Command hooks** (`type: "command"`): print the path as the last non-empty line of stdout. Claude Code strips ANSI escape codes before reading that line, so shell startup banners printed before your `echo` are ignored. Redirect any other hook output to stderr.
2279
2769
  - **HTTP hooks** (`type: "http"`): return `{ "hookSpecificOutput": { "hookEventName": "WorktreeCreate", "worktreePath": "/absolute/path" } }` in the response body.
2280
2770
 
2281
- If the hook fails or produces no path, worktree creation fails with an error.Claude Code resolves a relative path against the directory the hook ran in. If the resulting path isn’t a directory Claude Code can enter, the session prints an error naming the path and exits with code 1. Before v2.1.205, a relative path or a path that didn’t exist on disk crashed the session at startup, and with `-p` it stalled for about 30 seconds before exiting with code 0.
2771
+ If the hook fails or produces no path, worktree creation fails with an error.Claude Code resolves a relative path against the directory the hook ran in, collapsing any `.` or `..` segments in it. If the resulting path isn’t a directory Claude Code can enter, the session prints an error naming the path and exits with code 1.Claude Code refuses an absolute path that contains `.` or `..` segments, and any path that passes through a symlink below the repository root, because a symlink committed to the repository could redirect the worktree outside it. The error names the rejected component. Return a normalized path that doesn’t pass through a symlink inside the repository. Before v2.1.216, worktree creation followed the hook’s path without this screening.
2282
2772
 
2283
2773
  ### [​](https://code.claude.com/docs/en/hooks\#worktreeremove) WorktreeRemove
2284
2774
 
2285
- Runs when a worktree is being removed, either when you exit a `--worktree` session and choose to remove it, or when a subagent with `isolation: "worktree"` finishes. This is the cleanup counterpart to [WorktreeCreate](https://code.claude.com/docs/en/hooks#worktreecreate).For git-based worktrees, Claude Code handles cleanup automatically with `git worktree remove`. If you configured a WorktreeCreate hook for a non-git version control system, pair it with a WorktreeRemove hook to handle cleanup. Without one, the worktree directory is left on disk.Claude Code passes the path returned by WorktreeCreate as `worktree_path` in the hook input. This example reads that path and removes the directory:
2775
+ Runs when Claude Code cleans up a worktree that your [`WorktreeCreate`](https://code.claude.com/docs/en/hooks#worktreecreate) hook created. The event fires when:
2776
+
2777
+ - You exit an interactive [worktree session](https://code.claude.com/docs/en/worktrees#start-claude-in-a-worktree) and choose to remove the worktree when Claude Code prompts you
2778
+ - You exit an interactive worktree session you haven’t [named](https://code.claude.com/docs/en/sessions#name-your-sessions), Claude Code finds no changed or untracked files, and it removes the worktree without prompting you
2779
+ - You delete a [background session](https://code.claude.com/docs/en/agent-view#what-deleting-a-session-removes) that runs in the worktree
2780
+ - You ask Claude to exit and remove the worktree during a session, and Claude removes it with the [`ExitWorktree`](https://code.claude.com/docs/en/tools-reference) tool
2781
+ - A session that [`claude remote-control`](https://code.claude.com/docs/en/remote-control#start-a-remote-control-session) started in `worktree` mode ends without crashing, and Claude Code finds no changed or untracked files and no new commits in its worktree
2782
+ - You stop `claude remote-control` while sessions it started in `worktree` mode are still running, and Claude Code finds no changed or untracked files and no new commits in their worktrees
2783
+
2784
+ When a subagent or a workflow agent finishes, Claude Code keeps the worktree your WorktreeCreate hook made for it and doesn’t run your WorktreeRemove hook. Remove those worktrees yourself when the work is done.Claude Code doesn’t prompt you before your hook runs for an `ExitWorktree` call or a `claude remote-control` cleanup. `ExitWorktree` refuses to remove a hook-created worktree unless the call passes [`discard_changes: true`](https://code.claude.com/docs/en/agent-sdk/typescript#exitworktree), which makes your hook the last check on that route.Claude Code uses git to look for changed or untracked files, so it finds none in a worktree that isn’t a git checkout or inside one, even when the directory holds uncommitted work. Check for that work in your WorktreeRemove hook before it deletes anything.For git-based worktrees, Claude Code handles cleanup automatically with `git worktree remove`. If you configured a WorktreeCreate hook, pair it with a WorktreeRemove hook to control cleanup:
2785
+
2786
+ - **No WorktreeRemove hook**: Claude Code falls back to git for a removal at exit or through `ExitWorktree`, and keeps the worktree when `claude remote-control` cleans up:
2787
+ - **You exit a worktree session, or Claude calls `ExitWorktree`**: Claude Code removes the path your WorktreeCreate hook returned as `git worktree remove --force` would, so a worktree git recognizes is removed. A worktree git doesn’t recognize, for example one your hook created with a non-git version control system, stays on disk.
2788
+ - **`claude remote-control` cleans up a session’s worktree**: the worktree stays on disk, and the terminal running `claude remote-control` shows `worktree removal failed, kept: <path>`.
2789
+ - **You delete a background session**: see [What deleting a session removes](https://code.claude.com/docs/en/agent-view#what-deleting-a-session-removes).
2790
+ - **Hook exits 0**: the worktree counts as removed. Claude Code reads nothing else from the hook, so make sure your hook deleted the directory.
2791
+ - **Hook exits non-zero**: the removal fails if the directory at `worktree_path` still exists afterward, and the worktree stays on disk with no git fallback. A hook that deleted the directory before exiting non-zero counts as removed. For how the failure is reported, see [WorktreeRemove input](https://code.claude.com/docs/en/hooks#worktreeremove-input).
2792
+
2793
+ Claude Code never deletes a branch belonging to a hook-created worktree, because it only knows the path your WorktreeCreate hook returned. If your WorktreeCreate hook creates a branch, delete it in your WorktreeRemove hook.Claude Code discards a WorktreeRemove hook’s [JSON output fields](https://code.claude.com/docs/en/hooks#json-output), such as `systemMessage` and `continue`.For a background-session delete, Claude Code verifies the stored worktree path before running the hook and refuses a path that is a symlink or passes through one below the repository root. On that route, the hook runs for a worktree that still contains files only when you confirm the delete in [agent view](https://code.claude.com/docs/en/agent-view#what-deleting-a-session-removes); for such a worktree, [`claude rm`](https://code.claude.com/docs/en/agent-view#manage-sessions-from-the-shell) keeps the session and worktree instead. Before v2.1.216, the hook ran on the stored path without these checks.Claude Code passes the path returned by WorktreeCreate as `worktree_path` in the hook input. This example reads that path and removes the directory:
2286
2794
 
2287
2795
  ```
2288
2796
  {
@@ -2315,7 +2823,11 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2315
2823
  }
2316
2824
  ```
2317
2825
 
2318
- WorktreeRemove hooks have no decision control. They can’t block worktree removal but can perform cleanup tasks like removing version control state or archiving changes. Hook failures are logged in debug mode only.
2826
+ A WorktreeRemove hook’s exit code decides the outcome. When a hook exits non-zero and the directory at `worktree_path` still exists afterward, the removal fails:
2827
+
2828
+ - The worktree stays on disk, and the hook’s command and stderr go to the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks).
2829
+ - If you were deleting a background session, the session stays too. The refusal message in [agent view](https://code.claude.com/docs/en/agent-view#what-deleting-a-session-removes) reports how the hook ended, such as `exited 1`, quotes the start of its stderr, and says whether deleting the session again removes the directory anyway.
2830
+ - If `claude remote-control` was cleaning up a session’s worktree, the terminal running it shows `worktree removal failed, kept: <path>`.
2319
2831
 
2320
2832
  ### [​](https://code.claude.com/docs/en/hooks\#precompact) PreCompact
2321
2833
 
@@ -2324,13 +2836,13 @@ Runs before Claude Code is about to run a compact operation.The matcher value in
2324
2836
  | Matcher | When it fires |
2325
2837
  | --- | --- |
2326
2838
  | `manual` | `/compact` |
2327
- | `auto` | Auto-compact when the context window is full |
2839
+ | `auto` | Auto-compact when the conversation reaches the [auto-compact window](https://code.claude.com/docs/en/model-config#set-the-auto-compact-window) |
2328
2840
 
2329
- Exit with code 2 to block compaction. For a manual `/compact`, the stderr message is shown to the user. You can also block by returning JSON with `"decision": "block"`.Blocking automatic compaction has different effects depending on when it fires. If compaction was triggered proactively before the context limit, Claude Code skips it and the conversation continues uncompacted. If compaction was triggered to recover from a context-limit error already returned by the API, the underlying error surfaces and the current request fails.
2841
+ Exit with code 2 to block compaction. For a manual `/compact`, the stderr message is shown to the user. You can also block by returning JSON with `"decision": "block"`.Blocking automatic compaction has different effects depending on when it fires. If compaction was triggered proactively before the context limit, Claude Code skips it and the conversation continues uncompacted. If compaction was triggered to recover from a context-limit error already returned by the API, the underlying error surfaces and the current request fails.Claude Code discards a PreCompact hook’s `systemMessage` and `continue` fields.
2330
2842
 
2331
2843
  #### [​](https://code.claude.com/docs/en/hooks\#precompact-input) PreCompact input
2332
2844
 
2333
- In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PreCompact hooks receive `trigger` and `custom_instructions`. For `manual`, `custom_instructions` contains what the user passes into `/compact`. For `auto`, `custom_instructions` is empty.
2845
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PreCompact hooks receive `trigger` and `custom_instructions`. For `manual`, `custom_instructions` contains what the user passes into `/compact` and is `null` when they pass nothing. For `auto`, `custom_instructions` is `null`.
2334
2846
 
2335
2847
  ```
2336
2848
  {
@@ -2339,18 +2851,18 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2339
2851
  "cwd": "/Users/...",
2340
2852
  "hook_event_name": "PreCompact",
2341
2853
  "trigger": "manual",
2342
- "custom_instructions": ""
2854
+ "custom_instructions": null
2343
2855
  }
2344
2856
  ```
2345
2857
 
2346
2858
  ### [​](https://code.claude.com/docs/en/hooks\#postcompact) PostCompact
2347
2859
 
2348
- Runs after Claude Code completes a compact operation. Use this event to react to the new compacted state, for example to log the generated summary or update external state.The same matcher values apply as for `PreCompact`:
2860
+ Runs after Claude Code completes a compact operation. Use this event to react to the new compacted state, for example to log the generated summary or update external state. Claude Code discards a PostCompact hook’s `systemMessage` and `continue` fields.The same matcher values apply as for `PreCompact`:
2349
2861
 
2350
2862
  | Matcher | When it fires |
2351
2863
  | --- | --- |
2352
2864
  | `manual` | After `/compact` |
2353
- | `auto` | After auto-compact when the context window is full |
2865
+ | `auto` | After auto-compact when the conversation reaches the [auto-compact window](https://code.claude.com/docs/en/model-config#set-the-auto-compact-window) |
2354
2866
 
2355
2867
  #### [​](https://code.claude.com/docs/en/hooks\#postcompact-input) PostCompact input
2356
2868
 
@@ -2369,6 +2881,187 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2369
2881
 
2370
2882
  PostCompact hooks have no decision control. They can’t affect the compaction result but can perform follow-up tasks.
2371
2883
 
2884
+ ### [​](https://code.claude.com/docs/en/hooks\#premodelswitch) PreModelSwitch
2885
+
2886
+ Runs before Claude Code applies a model switch that you or a client requested. Use it to block a switch, require confirmation, or show what the switch will cost before it happens.PreModelSwitch requires Claude Code v2.1.251 or later. Claude Code runs it for these requests:
2887
+
2888
+ - `/model <name>` and the `/model` picker
2889
+ - The `Option+P` or `Alt+P` model picker
2890
+ - The Model setting in `/config`
2891
+ - Turning on [fast mode](https://code.claude.com/docs/en/fast-mode) when that changes the session’s model
2892
+ - A `set_model` request, or a model change in an `apply_flag_settings` request, from an [Agent SDK](https://code.claude.com/docs/en/agent-sdk/typescript#query-object) host or [Remote Control](https://code.claude.com/docs/en/remote-control)
2893
+
2894
+ Claude Code doesn’t run PreModelSwitch hooks for switches it makes on its own, such as an [automatic model fallback](https://code.claude.com/docs/en/model-config#automatic-model-fallback) or restoring the model when you resume a session. Those changes reach [PostModelSwitch](https://code.claude.com/docs/en/hooks#postmodelswitch) only.Claude Code compares the matcher against the canonical name of the model the session is switching to, ignoring any `[1m]` suffix. An alias such as `opus`, a dated model ID, and a provider-specific ID such as an Amazon Bedrock model ID all match the one canonical name they resolve to, so `claude-opus-5` covers every spelling of Opus 5.When Claude Code can’t determine a canonical name for the target, for example a custom model ID that only your [LLM gateway](https://code.claude.com/docs/en/llm-gateway) knows, it runs every PreModelSwitch hook regardless of matcher. A hook that blocks should therefore check `to_model` from its input rather than rely on the matcher alone.Write 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:
2895
+
2896
+ - macOS/Linux
2897
+
2898
+ - Windows (PowerShell)
2899
+
2900
+
2901
+ The command checks `to_model` with `jq`:
2902
+
2903
+ ```
2904
+ {
2905
+ "hooks": {
2906
+ "PreModelSwitch": [\
2907
+ {\
2908
+ "matcher": "claude-opus-4-6",\
2909
+ "hooks": [\
2910
+ {\
2911
+ "type": "command",\
2912
+ "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"\
2913
+ }\
2914
+ ]\
2915
+ }\
2916
+ ]
2917
+ }
2918
+ }
2919
+ ```
2920
+
2921
+ Register a command hook that runs a script through PowerShell:
2922
+
2923
+ ```
2924
+ {
2925
+ "hooks": {
2926
+ "PreModelSwitch": [\
2927
+ {\
2928
+ "matcher": "claude-opus-4-6",\
2929
+ "hooks": [\
2930
+ {\
2931
+ "type": "command",\
2932
+ "command": "powershell.exe",\
2933
+ "args": [\
2934
+ "-NoProfile",\
2935
+ "-ExecutionPolicy",\
2936
+ "Bypass",\
2937
+ "-File",\
2938
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-opus-46.ps1"\
2939
+ ]\
2940
+ }\
2941
+ ]\
2942
+ }\
2943
+ ]
2944
+ }
2945
+ }
2946
+ ```
2947
+
2948
+ Save this script to `.claude/hooks/block-opus-46.ps1` in your project:
2949
+
2950
+ ```
2951
+ $hookInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
2952
+ if ($hookInput.to_model -match 'opus-4-6') {
2953
+ [Console]::Error.WriteLine('Opus 4.6 is retired for this project. Use a newer model.')
2954
+ exit 2
2955
+ }
2956
+ exit 0
2957
+ ```
2958
+
2959
+ To 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.
2960
+
2961
+ #### [​](https://code.claude.com/docs/en/hooks\#premodelswitch-input) PreModelSwitch input
2962
+
2963
+ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#common-input-fields), PreModelSwitch hooks receive the fields in this table. The last five describe what re-sending the conversation to the new model costs, so a hook can show that figure before the switch happens.
2964
+
2965
+ | Field | Type | Description |
2966
+ | --- | --- | --- |
2967
+ | `from_model` | string | Model ID the switch changes from |
2968
+ | `to_model` | string | Model ID the switch changes to. The matcher compares against this model’s canonical name |
2969
+ | `requested_model` | string or `null` | The model the request named: an alias such as `opus`, a full model ID, or `null` when the request was for the default model |
2970
+ | `source` | string | Where the request came from: `"command"` for `/model <name>`, the Model setting in `/config`, or turning on fast mode; `"picker"` for a model picker; `"sdk"` for a `set_model` request, or a model change in an `apply_flag_settings` request, from an Agent SDK host or Remote Control |
2971
+ | `context_tokens` | number | Tokens the next request re-sends as its prompt: the input, cache read, cache creation, and output tokens of the last response in the main conversation, combined. `0` before the first response |
2972
+ | `prompt_cache_warm` | boolean | Whether the current model’s prompt cache is likely still warm, meaning the switch forfeits it |
2973
+ | `cache_ttl` | string | [Prompt cache lifetime](https://code.claude.com/docs/en/prompt-caching#cache-lifetime) Claude Code requests for this session: `"5m"` or `"1h"` |
2974
+ | `estimated_cache_write_usd` | number | Estimated cost in US dollars of writing `context_tokens` to the prompt cache on `to_model` at the `cache_ttl` rate, excluding the next response. The server may not need to re-cache the whole context, so treat it as an estimate |
2975
+ | `pricing` | string | How Claude Code priced `estimated_cache_write_usd`: `"configured"` at your organization’s own rates when it has configured them, `"catalog"` at list price, or `"default"` when `to_model` has no known price and Claude Code assumed a default rate |
2976
+
2977
+ This example shows the input for `/model opus` in a session running Sonnet 5:
2978
+
2979
+ ```
2980
+ {
2981
+ "session_id": "abc123",
2982
+ "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
2983
+ "cwd": "/Users/...",
2984
+ "hook_event_name": "PreModelSwitch",
2985
+ "from_model": "claude-sonnet-5",
2986
+ "to_model": "claude-opus-5",
2987
+ "requested_model": "opus",
2988
+ "source": "command",
2989
+ "context_tokens": 182340,
2990
+ "prompt_cache_warm": true,
2991
+ "cache_ttl": "5m",
2992
+ "estimated_cache_write_usd": 1.1396,
2993
+ "pricing": "catalog"
2994
+ }
2995
+ ```
2996
+
2997
+ #### [​](https://code.claude.com/docs/en/hooks\#premodelswitch-decision-control) PreModelSwitch decision control
2998
+
2999
+ `PreModelSwitch` hooks can cancel the switch, ask the user to confirm it, or let it proceed. Exit code 2 or a top-level `decision: "block"` cancels the switch.For finer control, return `permissionDecision` and `permissionDecisionReason` in a `hookSpecificOutput` object, as on [PreToolUse](https://code.claude.com/docs/en/hooks#pretooluse-decision-control). `PreModelSwitch` accepts `"allow"`, `"deny"`, and `"ask"`. It doesn’t accept `"defer"`, `updatedInput`, or `additionalContext`. The table below describes both fields:
3000
+
3001
+ | Field | Description |
3002
+ | --- | --- |
3003
+ | `permissionDecision` | `"allow"` proceeds and skips the [confirmation Claude Code shows while the prompt cache is warm](https://code.claude.com/docs/en/prompt-caching#switching-models). `"deny"` cancels the switch. `"ask"` prompts the user to confirm it |
3004
+ | `permissionDecisionReason` | For `"deny"`, shown to the user as the reason the switch was blocked, or returned as the error for a `set_model` request. For `"ask"`, shown in the confirmation prompt. Ignored for `"allow"` |
3005
+
3006
+ Only `/model` in an interactive session can show the `"ask"` prompt. On every other surface, including non-interactive mode with the `-p` flag, `/config`, and `set_model` requests, Claude Code treats `"ask"` as a refusal.This example asks the user to confirm and quotes the token count from `context_tokens`:
3007
+
3008
+ ```
3009
+ {
3010
+ "hookSpecificOutput": {
3011
+ "hookEventName": "PreModelSwitch",
3012
+ "permissionDecision": "ask",
3013
+ "permissionDecisionReason": "Switching now re-sends about 180k tokens to the new model. Continue?"
3014
+ }
3015
+ }
3016
+ ```
3017
+
3018
+ When multiple PreModelSwitch hooks return different decisions, precedence is `deny` \> `ask` \> `allow`.Claude Code shows the user any `systemMessage` your hook returns regardless of the decision, so a cost-report hook can return `{"systemMessage": "..."}` and exit 0.A PreModelSwitch hook that doesn’t respond before its timeout blocks the switch. For what a timeout does on other events, see [Timeouts](https://code.claude.com/docs/en/hooks#timeouts). The default timeout for this event is 30 seconds. `PreModelSwitch` runs `command`, `http`, and `mcp_tool` hooks only, so the `prompt` and `agent` defaults don’t apply.A hook that exits with a code other than 0 or 2 and prints no JSON decision is a non-blocking error, as described under [Other exit codes](https://code.claude.com/docs/en/hooks#other-exit-codes).
3019
+
3020
+ ### [​](https://code.claude.com/docs/en/hooks\#postmodelswitch) PostModelSwitch
3021
+
3022
+ Runs after the session’s model changes. Use it to give Claude model-specific guidance without editing every CLAUDE.md, for example an organization-wide instruction that applies on certain models.PostModelSwitch requires Claude Code v2.1.251 or later. It can’t block, because the model has already changed. Claude Code runs PostModelSwitch hooks after any of these changes:
3023
+
3024
+ - A switch you or a client requested
3025
+ - An [automatic model fallback](https://code.claude.com/docs/en/model-config#automatic-model-fallback), which changes the session’s model
3026
+ - A setting such as [`opusplan`](https://code.claude.com/docs/en/model-config#opusplan-model-setting) entering or leaving plan mode
3027
+ - Claude Code restoring the model when you resume a session
3028
+
3029
+ Claude Code doesn’t run PostModelSwitch hooks when a model from a [fallback model chain](https://code.claude.com/docs/en/model-config#fallback-model-chains) serves a turn, because that substitution lasts one turn and leaves the session’s model unchanged.The matcher follows the same rules as [PreModelSwitch](https://code.claude.com/docs/en/hooks#premodelswitch): Claude Code compares it against the canonical name of the model the session switched to.This example adds guidance whenever the session’s model changes to any Opus model:
3030
+
3031
+ ```
3032
+ {
3033
+ "hooks": {
3034
+ "PostModelSwitch": [\
3035
+ {\
3036
+ "matcher": ".*opus.*",\
3037
+ "hooks": [\
3038
+ {\
3039
+ "type": "command",\
3040
+ "command": "echo 'On Opus, delegate implementation work to subagents and keep this conversation for planning and review.'"\
3041
+ }\
3042
+ ]\
3043
+ }\
3044
+ ]
3045
+ }
3046
+ }
3047
+ ```
3048
+
3049
+ To confirm the hook works, switch to an Opus model from a session running a different model, for example run `/model opus` from a Sonnet session, then ask Claude what guidance it has about the current model.
3050
+
3051
+ #### [​](https://code.claude.com/docs/en/hooks\#postmodelswitch-input) PostModelSwitch input
3052
+
3053
+ PostModelSwitch hooks receive the same fields as [PreModelSwitch](https://code.claude.com/docs/en/hooks#premodelswitch-input), with `hook_event_name` set to `"PostModelSwitch"` and two more `source` values: `"auto"` for an automatic fallback or other change Claude Code made on its own, and `"resume"` for the model restored when you resume a session.`requested_model` is `null` when `source` is `"auto"`. When `source` is `"resume"`, it is the saved model setting Claude Code restored.
3054
+
3055
+ #### [​](https://code.claude.com/docs/en/hooks\#postmodelswitch-decision-control) PostModelSwitch decision control
3056
+
3057
+ Claude Code takes your hook’s [plain-text stdout](https://code.claude.com/docs/en/hooks#exit-code-0) on exit 0, or `additionalContext` from JSON output, and delivers it to Claude with the next request after the switch. In addition to the [JSON output fields](https://code.claude.com/docs/en/hooks#json-output) available to all hooks, you can return:
3058
+
3059
+ | Field | Description |
3060
+ | --- | --- |
3061
+ | `additionalContext` | String added to Claude’s context with the next request. See [Add context for Claude](https://code.claude.com/docs/en/hooks#add-context-for-claude) |
3062
+
3063
+ If the hook hasn’t finished within five seconds after you send the next prompt, Claude Code sends that request without the output and attaches it to the following request instead. If the model changes several times before the next request, Claude Code delivers only the output for the last switch’s target model.
3064
+
2372
3065
  ### [​](https://code.claude.com/docs/en/hooks\#sessionend) SessionEnd
2373
3066
 
2374
3067
  Runs when a Claude Code session ends. Useful for cleanup tasks, logging session
@@ -2380,8 +3073,8 @@ statistics, or saving session state. Supports matchers to filter by exit reason.
2380
3073
  | `resume` | Session switched via interactive `/resume` |
2381
3074
  | `logout` | User logged out |
2382
3075
  | `prompt_input_exit` | User exited while prompt input was visible |
2383
- | `bypass_permissions_disabled` | Bypass permissions mode was disabled |
2384
3076
  | `other` | Other exit reasons |
3077
+ | `bypass_permissions_disabled` | Removed in v2.1.234; Claude Code doesn’t send it. Drop it from your `SessionEnd` matchers |
2385
3078
 
2386
3079
  #### [​](https://code.claude.com/docs/en/hooks\#sessionend-input) SessionEnd input
2387
3080
 
@@ -2397,15 +3090,22 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2397
3090
  }
2398
3091
  ```
2399
3092
 
2400
- SessionEnd hooks have no decision control. They can’t block session termination but can perform cleanup tasks.SessionEnd hooks have a default timeout of 1.5 seconds. This applies to session exit, `/clear`, and switching sessions via interactive `/resume`. If a hook needs more time, set a per-hook `timeout` in the hook configuration. The overall budget is automatically raised to the highest per-hook timeout configured in settings files, up to 60 seconds. Timeouts set on plugin-provided hooks don’t raise the budget. To override the budget explicitly, set the `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS` environment variable in milliseconds.
3093
+ SessionEnd hooks have no decision control. They can’t block session termination but can perform cleanup tasks. Claude Code discards their [JSON output fields](https://code.claude.com/docs/en/hooks#json-output), such as `systemMessage`.SessionEnd hooks have a default timeout of 1.5 seconds. It applies when you exit, run `/clear`, or switch sessions with interactive `/resume`. You can give a hook more time in two ways:
3094
+
3095
+ - **Per-hook `timeout`**: set `timeout` in that hook’s configuration. The overall budget rises automatically to match the highest per-hook `timeout` in your settings files, up to 60 seconds. If you raise the budget this way, a hook without its own `timeout` still keeps the default. Timeouts set on plugin-provided hooks don’t raise the budget.
3096
+ - **`CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS`**: set this environment variable in milliseconds to override the budget explicitly. The value you set also becomes the timeout for each hook without its own `timeout`.
3097
+
3098
+ This example sets the budget to 5 seconds:
2401
3099
 
2402
3100
  ```
2403
3101
  CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=5000 claude
2404
3102
  ```
2405
3103
 
3104
+ Before v2.1.268, `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS` raised only the overall budget, and a hook without its own `timeout` was still canceled after 1.5 seconds.
3105
+
2406
3106
  ### [​](https://code.claude.com/docs/en/hooks\#elicitation) Elicitation
2407
3107
 
2408
- Runs when an MCP server requests user input mid-task. By default, Claude Code shows an interactive dialog for the user to respond. Hooks can intercept this request and respond programmatically, skipping the dialog entirely.The matcher field matches against the MCP server name.
3108
+ Runs when an MCP server requests user input mid-task. By default, Claude Code shows an interactive dialog for the user to respond. Hooks can intercept this request and respond programmatically, skipping the dialog entirely.For a complete hook with its settings entry and script, see [Answer a form request from a script](https://code.claude.com/docs/en/hooks#answer-a-form-request-from-a-script).The matcher field matches against the MCP server name.
2409
3109
 
2410
3110
  #### [​](https://code.claude.com/docs/en/hooks\#elicitation-input) Elicitation input
2411
3111
 
@@ -2416,7 +3116,6 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2416
3116
  "session_id": "abc123",
2417
3117
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
2418
3118
  "cwd": "/Users/...",
2419
- "permission_mode": "default",
2420
3119
  "hook_event_name": "Elicitation",
2421
3120
  "mcp_server_name": "my-mcp-server",
2422
3121
  "message": "Please provide your credentials",
@@ -2437,7 +3136,6 @@ For URL-mode elicitation, used for browser-based authentication:
2437
3136
  "session_id": "abc123",
2438
3137
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
2439
3138
  "cwd": "/Users/...",
2440
- "permission_mode": "default",
2441
3139
  "hook_event_name": "Elicitation",
2442
3140
  "mcp_server_name": "my-mcp-server",
2443
3141
  "message": "Please authenticate",
@@ -2448,7 +3146,16 @@ For URL-mode elicitation, used for browser-based authentication:
2448
3146
 
2449
3147
  #### [​](https://code.claude.com/docs/en/hooks\#elicitation-output) Elicitation output
2450
3148
 
2451
- To respond programmatically without showing the dialog, return a JSON object with `hookSpecificOutput`:
3149
+ An Elicitation hook can answer the request for the user, decline or cancel it, or leave it to the dialog. To answer, decline, or cancel, exit 0 and print a `hookSpecificOutput` object with an `action`. The server gets your answer and no dialog appears. Each row of this table shows what to return for one outcome and what the MCP server receives:
3150
+
3151
+ | To | Return | The server receives |
3152
+ | --- | --- | --- |
3153
+ | Answer for the user | `"action": "accept"`, with the form field values in `content` | `accept` with your `content` |
3154
+ | Decline the request | `"action": "decline"` | `decline` |
3155
+ | Cancel the request | `"action": "cancel"` | `cancel` |
3156
+ | Leave the request to the user | No output, with exit code 0 | The user’s answer from the [dialog](https://code.claude.com/docs/en/mcp#respond-to-mcp-elicitation-requests) |
3157
+
3158
+ This output answers the form-mode request shown under [Elicitation input](https://code.claude.com/docs/en/hooks#elicitation-input). The keys in `content` are the property names from that request’s `requested_schema`:
2452
3159
 
2453
3160
  ```
2454
3161
  {
@@ -2462,16 +3169,128 @@ To respond programmatically without showing the dialog, return a JSON object wit
2462
3169
  }
2463
3170
  ```
2464
3171
 
2465
- | Field | Values | Description |
2466
- | --- | --- | --- |
2467
- | `action` | `accept`, `decline`, `cancel` | Whether to accept, decline, or cancel the request |
2468
- | `content` | object | Form field values to submit. Only used when `action` is `accept` |
3172
+ This output declines a request:
3173
+
3174
+ ```
3175
+ {
3176
+ "hookSpecificOutput": {
3177
+ "hookEventName": "Elicitation",
3178
+ "action": "decline"
3179
+ }
3180
+ }
3181
+ ```
3182
+
3183
+ In the dialog, selecting **Decline** sends `decline` and pressing `Esc` sends `cancel`, so return the one you want the server to see.For a URL-mode request, a hook that returns `accept` skips the dialog, so the URL never opens.Claude Code discards `reason`, `systemMessage`, and `continue` from an Elicitation hook’s JSON output, whichever `action` you return.
3184
+
3185
+ #### [​](https://code.claude.com/docs/en/hooks\#other-ways-to-decline-an-elicitation) Other ways to decline an elicitation
3186
+
3187
+ Your hook can also decline in these ways. The server receives the same `decline` as for `"action": "decline"`:
2469
3188
 
2470
- Exit code 2 denies the elicitation and shows stderr to the user.
3189
+ - **Exits with code 2**: Claude Code ignores a `hookSpecificOutput` printed by the same hook
3190
+ - **Prints a top-level `"decision": "block"`**: the block overrides an `action` in the same output
3191
+
3192
+ When several hooks match the same request, a decline from one of them overrides an `accept` or `cancel` from another.This script declines URL-mode requests and leaves form requests to the dialog:
3193
+
3194
+ ```
3195
+ #!/bin/bash
3196
+ if [ "$(jq -r '.mode')" = "url" ]; then
3197
+ exit 2
3198
+ fi
3199
+ ```
3200
+
3201
+ Neither the user nor the server sees why your hook declined, because Claude Code doesn’t show your stderr or your `reason`.Claude Code ignored a top-level `decision` from `Elicitation` and `ElicitationResult` hooks from v2.1.105 until the fix in v2.1.284.
3202
+
3203
+ #### [​](https://code.claude.com/docs/en/hooks\#answer-a-form-request-from-a-script) Answer a form request from a script
3204
+
3205
+ This 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.
3206
+
3207
+ - macOS/Linux
3208
+
3209
+ - Windows (PowerShell)
3210
+
3211
+
3212
+ Register a command hook for the event in your settings file, with the server name as the matcher:
3213
+
3214
+ ```
3215
+ {
3216
+ "hooks": {
3217
+ "Elicitation": [\
3218
+ {\
3219
+ "matcher": "issue-tracker",\
3220
+ "hooks": [\
3221
+ {\
3222
+ "type": "command",\
3223
+ "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.sh",\
3224
+ "args": []\
3225
+ }\
3226
+ ]\
3227
+ }\
3228
+ ]
3229
+ }
3230
+ }
3231
+ ```
3232
+
3233
+ Save this script to `.claude/hooks/answer-project-key.sh` in your project and make it executable with `chmod +x`:
3234
+
3235
+ ```
3236
+ #!/bin/bash
3237
+ input=$(cat)
3238
+ fields=$(jq -c '.requested_schema.properties // {} | keys' <<<"$input")
3239
+
3240
+ if [ "$fields" = '["project_key"]' ]; then
3241
+ jq -n '{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept", content: {project_key: "DOCS"}}}'
3242
+ fi
3243
+ ```
3244
+
3245
+ Register a command hook that runs the script through PowerShell, with the server name as the matcher:
3246
+
3247
+ ```
3248
+ {
3249
+ "hooks": {
3250
+ "Elicitation": [\
3251
+ {\
3252
+ "matcher": "issue-tracker",\
3253
+ "hooks": [\
3254
+ {\
3255
+ "type": "command",\
3256
+ "command": "powershell.exe",\
3257
+ "args": [\
3258
+ "-NoProfile",\
3259
+ "-ExecutionPolicy",\
3260
+ "Bypass",\
3261
+ "-File",\
3262
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.ps1"\
3263
+ ]\
3264
+ }\
3265
+ ]\
3266
+ }\
3267
+ ]
3268
+ }
3269
+ }
3270
+ ```
3271
+
3272
+ Save this script to `.claude/hooks/answer-project-key.ps1` in your project:
3273
+
3274
+ ```
3275
+ $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
3276
+ $fields = @($request.requested_schema.properties.PSObject.Properties.Name)
3277
+
3278
+ if ($fields.Count -eq 1 -and $fields[0] -eq 'project_key') {
3279
+ @{
3280
+ hookSpecificOutput = @{
3281
+ hookEventName = "Elicitation"
3282
+ action = "accept"
3283
+ content = @{ project_key = "DOCS" }
3284
+ }
3285
+ } | ConvertTo-Json -Depth 3
3286
+ }
3287
+ ```
3288
+
3289
+ To 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](https://code.claude.com/docs/en/hooks#debug-hooks) has a line that ends with `Elicitation resolved by hook: {"action":"accept","content":{"project_key":"DOCS"}}`.
2471
3290
 
2472
3291
  ### [​](https://code.claude.com/docs/en/hooks\#elicitationresult) ElicitationResult
2473
3292
 
2474
- Runs after a user responds to an MCP elicitation. Hooks can observe, modify, or block the response before it is sent back to the MCP server.The matcher field matches against the MCP server name.
3293
+ Runs after a user responds to an MCP elicitation. Hooks can observe, modify, or block the response before it is sent back to the MCP server.When an [Elicitation](https://code.claude.com/docs/en/hooks#elicitation) hook answers a request, Claude Code sends that answer to the server without running ElicitationResult hooks.The matcher field matches against the MCP server name.
2475
3294
 
2476
3295
  #### [​](https://code.claude.com/docs/en/hooks\#elicitationresult-input) ElicitationResult input
2477
3296
 
@@ -2482,43 +3301,68 @@ In addition to the [common input fields](https://code.claude.com/docs/en/hooks#c
2482
3301
  "session_id": "abc123",
2483
3302
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
2484
3303
  "cwd": "/Users/...",
2485
- "permission_mode": "default",
2486
3304
  "hook_event_name": "ElicitationResult",
2487
3305
  "mcp_server_name": "my-mcp-server",
2488
3306
  "action": "accept",
2489
3307
  "content": { "username": "alice" },
2490
- "mode": "form",
2491
- "elicitation_id": "elicit-123"
3308
+ "mode": "form"
2492
3309
  }
2493
3310
  ```
2494
3311
 
2495
3312
  #### [​](https://code.claude.com/docs/en/hooks\#elicitationresult-output) ElicitationResult output
2496
3313
 
2497
- To override the user’s response, return a JSON object with `hookSpecificOutput`:
3314
+ An ElicitationResult hook can let the user’s response through, change its values, or block it. To change or block the response, exit 0 and print a `hookSpecificOutput` object with an `action`. Each row of this table shows what to return for one outcome and what the MCP server receives:
3315
+
3316
+ | To | Return | The server receives |
3317
+ | --- | --- | --- |
3318
+ | Let the response through | No output, with exit code 0 | The user’s response, unchanged |
3319
+ | Change the submitted values | `"action": "accept"`, with the new values in `content` | `accept` with your `content` in place of the user’s values |
3320
+ | Block the response | `"action": "decline"` | `decline`, without the user’s values |
3321
+ | Cancel the request | `"action": "cancel"` | `cancel`, along with the values the user submitted. To withhold them, return `"decline"` |
3322
+
3323
+ This output changes the response shown under [ElicitationResult input](https://code.claude.com/docs/en/hooks#elicitationresult-input), so the server receives `alice@example.com` where the user submitted `alice`:
2498
3324
 
2499
3325
  ```
2500
3326
  {
2501
3327
  "hookSpecificOutput": {
2502
3328
  "hookEventName": "ElicitationResult",
2503
- "action": "decline",
2504
- "content": {}
3329
+ "action": "accept",
3330
+ "content": {
3331
+ "username": "alice@example.com"
3332
+ }
2505
3333
  }
2506
3334
  }
2507
3335
  ```
2508
3336
 
2509
- | Field | Values | Description |
2510
- | --- | --- | --- |
2511
- | `action` | `accept`, `decline`, `cancel` | Overrides the user’s action |
2512
- | `content` | object | Overrides form field values. Only meaningful when `action` is `accept` |
3337
+ Your `content` replaces the user’s whole `content` object, so include the fields you aren’t changing. Return `action` along with it, because Claude Code ignores a `hookSpecificOutput` that has no `action`.ElicitationResult hooks also run when the user declines or cancels, and your `action` replaces theirs. Check that the input’s `action` is `accept` before you return `accept`, or your hook turns a declined request into an accepted one. This script makes the same change when the user accepted, keeps the other fields, and prints nothing otherwise:
3338
+
3339
+ ```
3340
+ #!/bin/bash
3341
+ input=$(cat)
2513
3342
 
2514
- Exit code 2 blocks the response, changing the effective action to `decline`.
3343
+ if [ "$(jq -r '.action' <<<"$input")" = "accept" ]; then
3344
+ jq '{hookSpecificOutput: {hookEventName: "ElicitationResult", action: "accept", content: (.content + {username: (.content.username + "@example.com")})}}' <<<"$input"
3345
+ fi
3346
+ ```
3347
+
3348
+ This output blocks the response:
3349
+
3350
+ ```
3351
+ {
3352
+ "hookSpecificOutput": {
3353
+ "hookEventName": "ElicitationResult",
3354
+ "action": "decline"
3355
+ }
3356
+ }
3357
+ ```
3358
+
3359
+ Exit code 2 and a top-level `"decision": "block"` also block the response. [Other ways to decline an elicitation](https://code.claude.com/docs/en/hooks#other-ways-to-decline-an-elicitation) covers which one takes effect when a hook combines them, what the user sees, and which versions ignored `decision`.Claude Code discards `reason`, `systemMessage`, and `continue` from an ElicitationResult hook’s JSON output, whichever `action` you return.
2515
3360
 
2516
3361
  ## [​](https://code.claude.com/docs/en/hooks\#prompt-based-hooks) Prompt-based hooks
2517
3362
 
2518
3363
  In addition to command, HTTP, and MCP tool hooks, Claude Code supports prompt-based hooks (`type: "prompt"`) that use an LLM to evaluate whether to allow or block an action, and agent hooks (`type: "agent"`) that spawn an agentic verifier with tool access. Not all events support every hook type.Events that support all five hook types (`command`, `http`, `mcp_tool`, `prompt`, and `agent`):
2519
3364
 
2520
3365
  - `PermissionDenied`
2521
- - `PermissionRequest`
2522
3366
  - `PostToolBatch`
2523
3367
  - `PostToolUse`
2524
3368
  - `PostToolUseFailure`
@@ -2531,36 +3375,40 @@ In addition to command, HTTP, and MCP tool hooks, Claude Code supports prompt-ba
2531
3375
  - `UserPromptExpansion`
2532
3376
  - `UserPromptSubmit`
2533
3377
 
2534
- Events that support `command`, `http`, and `mcp_tool` hooks but not `prompt` or `agent`:
3378
+ `PermissionRequest` supports `command`, `http`, `mcp_tool`, and `prompt` hooks but not `agent` hooks. If you configure an agent hook on this event, Claude Code skips it and the permission flow proceeds unchanged. To allow or deny from a hook, return the [decision object](https://code.claude.com/docs/en/hooks#permissionrequest-decision-control) from a command or HTTP hook.Events that support `command`, `http`, and `mcp_tool` hooks but not `prompt` or `agent`:
2535
3379
 
2536
3380
  - `ConfigChange`
2537
3381
  - `CwdChanged`
3382
+ - `DirectoryAdded`
2538
3383
  - `Elicitation`
2539
3384
  - `ElicitationResult`
2540
3385
  - `FileChanged`
2541
3386
  - `InstructionsLoaded`
3387
+ - `MessageDisplay`
2542
3388
  - `Notification`
2543
3389
  - `PostCompact`
3390
+ - `PostModelSwitch`
2544
3391
  - `PreCompact`
3392
+ - `PreModelSwitch`
2545
3393
  - `SessionEnd`
2546
3394
  - `StopFailure`
2547
3395
  - `SubagentStart`
2548
3396
  - `WorktreeCreate`
2549
3397
  - `WorktreeRemove`
2550
3398
 
2551
- `SessionStart` and `Setup` support `command` and `mcp_tool` hooks. They don’t support `http`, `prompt`, or `agent` hooks.
3399
+ `SessionStart` and `Setup` support `command` and `mcp_tool` hooks, and [MCP tool hook fields](https://code.claude.com/docs/en/hooks#mcp-tool-hook-fields) describes when their `mcp_tool` hooks run. They don’t support `http`, `prompt`, or `agent` hooks.
2552
3400
 
2553
3401
  ### [​](https://code.claude.com/docs/en/hooks\#how-prompt-based-hooks-work) How prompt-based hooks work
2554
3402
 
2555
3403
  Instead of executing a Bash command, prompt-based hooks:
2556
3404
 
2557
- 1. Send the hook input and your prompt to a Claude model, Haiku by default
3405
+ 1. Send the hook input and your prompt to a Claude model, by default the one Claude Code uses for [background functionality](https://code.claude.com/docs/en/costs#background-token-usage)
2558
3406
  2. The LLM responds with structured JSON containing a decision
2559
3407
  3. Claude Code processes the decision automatically
2560
3408
 
2561
3409
  ### [​](https://code.claude.com/docs/en/hooks\#prompt-hook-configuration) Prompt hook configuration
2562
3410
 
2563
- Set `type` to `"prompt"` and provide a `prompt` string instead of a `command`. Use the `$ARGUMENTS` placeholder to inject the hook’s JSON input data into your prompt text. Claude Code sends the combined prompt and input to a fast Claude model, which returns a JSON decision.This `Stop` hook asks the LLM to evaluate whether all tasks are complete before allowing Claude to finish:
3411
+ Set `type` to `"prompt"` and provide a `prompt` string instead of a `command`. Use the `$ARGUMENTS` placeholder to inject the hook’s JSON input data into your prompt text.In a prompt or [agent hook](https://code.claude.com/docs/en/hooks#agent-based-hooks), you can write the `prompt` as a rule about what to block or allow, such as “Block any Bash command that reads `.env` files”, or as a condition that must hold, such as “All unit tests pass”.This `Stop` hook asks the LLM to evaluate whether all tasks are complete before allowing Claude to finish:
2564
3412
 
2565
3413
  ```
2566
3414
  {
@@ -2583,9 +3431,9 @@ Set `type` to `"prompt"` and provide a `prompt` string instead of a `command`. U
2583
3431
  | --- | --- | --- |
2584
3432
  | `type` | yes | Must be `"prompt"` |
2585
3433
  | `prompt` | yes | The prompt text to send to the LLM. Use `$ARGUMENTS` as a placeholder for the hook input JSON. If `$ARGUMENTS` is not present, input JSON is appended to the prompt |
2586
- | `model` | no | Model to use for evaluation. Defaults to a fast model |
3434
+ | `model` | no | Model to use for evaluation. Defaults to the model Claude Code uses for [background functionality](https://code.claude.com/docs/en/costs#background-token-usage) |
2587
3435
  | `timeout` | no | Timeout in seconds. Default: 30 |
2588
- | `continueOnBlock` | no | When the prompt returns `ok: false`, feed the reason back to Claude and continue the turn instead of stopping. Default: `false`. Implemented as `continue: true` on the resulting `decision: "block"`. See [Response schema](https://code.claude.com/docs/en/hooks#response-schema) for per-event behavior |
3436
+ | `continueOnBlock` | no | On the events it applies to, `true` feeds an `ok: false` reason back to Claude and continues instead of ending the turn. Default: `false`. See [Response schema](https://code.claude.com/docs/en/hooks#response-schema) for per-event behavior |
2589
3437
 
2590
3438
  ### [​](https://code.claude.com/docs/en/hooks\#response-schema) Response schema
2591
3439
 
@@ -2594,22 +3442,25 @@ The LLM must respond with JSON containing:
2594
3442
  ```
2595
3443
  {
2596
3444
  "ok": true | false,
2597
- "reason": "Explanation for the decision"
3445
+ "reason": "Explanation for the decision",
3446
+ "impossible": true | false
2598
3447
  }
2599
3448
  ```
2600
3449
 
2601
3450
  | Field | Description |
2602
3451
  | --- | --- |
2603
- | `ok` | `true` to allow. `false` produces a `decision: "block"`. See the per-event behavior below |
2604
- | `reason` | Required when `ok` is `false`. Used as the block reason |
3452
+ | `ok` | `true` to allow. For `false`, see the per-event behavior below |
3453
+ | `reason` | Required when `ok` is `false` |
3454
+ | `impossible` | Optional. The model returns it with `ok: false` when it judges the condition can never be satisfied. On `Stop` and `SubagentStop`, Claude Code then lets the turn end instead of feeding the reason back. Agent hooks and other events ignore it |
2605
3455
 
2606
3456
  What happens on `ok: false` depends on the event:
2607
3457
 
2608
- - `Stop` and `SubagentStop`: the reason is fed back to Claude as its next instruction and the turn continues
2609
- - `PreToolUse`: the tool call is denied and the reason is returned to Claude as the tool error, equivalent to a command hook’s `permissionDecision: "deny"`
3458
+ - `Stop` and `SubagentStop`: the reason is fed back to Claude as its next instruction and the turn continues, unless the response also sets `impossible: true`, in which case Claude Code allows the stop and the turn ends
3459
+ - `PreToolUse`: the tool call is denied; by default the turn ends and the deny reason appears in the chat as a warning line. Set `continueOnBlock: true` to instead return the reason to Claude as the tool error so it can adjust and continue, equivalent to a command hook’s `permissionDecision: "deny"`. Before v2.1.210, the deny reason was returned to Claude as the tool error and the turn continued
2610
3460
  - `PostToolUse`: by default the turn ends and the reason appears in the chat as a warning line. Set `continueOnBlock: true` to feed the reason back to Claude and continue the turn instead
2611
3461
  - `PostToolBatch`, `UserPromptSubmit`, and `UserPromptExpansion`: the turn ends and the reason appears as a warning line. These events end the turn on `decision: "block"` regardless of `continue`
2612
- - `PostToolUseFailure`, `TaskCreated`, and `TaskCompleted`: the reason is returned to Claude as a tool error, similar to `PreToolUse`
3462
+ - `PostToolUseFailure` and `TaskCreated`: the reason is returned to Claude as a tool error and the turn continues, regardless of `continueOnBlock`
3463
+ - `TaskCompleted`: when it fires because a task is marked completed during a turn, the reason is returned to Claude as a tool error and the turn continues, regardless of `continueOnBlock`. When it fires because a teammate stops, it behaves like `TeammateIdle` and halts the teammate by default
2613
3464
  - `TeammateIdle`: by default the teammate stops and the reason appears as a warning line. Set `continueOnBlock: true` to feed the reason back to the teammate and keep it working instead
2614
3465
  - `PermissionRequest`: `ok: false` has no effect. To deny an approval from a hook, use a [command hook](https://code.claude.com/docs/en/hooks#command-hook-fields) returning `hookSpecificOutput.decision.behavior: "deny"`
2615
3466
  - `PermissionDenied`: `ok: false` has no effect because the denial already happened. The only output this event reads is `hookSpecificOutput.retry`, which prompt and agent hooks can’t set. They run on this event, but their output is discarded. Use a [command hook](https://code.claude.com/docs/en/hooks#command-hook-fields) to return `retry`
@@ -2618,7 +3469,7 @@ If you need finer control on any event, use a [command hook](https://code.claude
2618
3469
 
2619
3470
  ### [​](https://code.claude.com/docs/en/hooks\#check-multiple-conditions-before-stopping) Check multiple conditions before stopping
2620
3471
 
2621
- This `Stop` hook uses a detailed prompt to check three conditions before allowing Claude to stop. `SubagentStop` hooks use the same format to evaluate whether a [subagent](https://code.claude.com/docs/en/sub-agents) should stop. If `"ok"` is `false`, Claude continues working with the provided reason as its next instruction:
3472
+ This `Stop` hook uses a detailed prompt to check three conditions before allowing Claude to stop. `SubagentStop` hooks use the same format to evaluate whether a [subagent](https://code.claude.com/docs/en/sub-agents) should stop. If the model returns `"ok": false` because the condition isn’t met yet, Claude continues working with the provided reason as its next instruction:
2622
3473
 
2623
3474
  ```
2624
3475
  {
@@ -2642,7 +3493,7 @@ This `Stop` hook uses a detailed prompt to check three conditions before allowin
2642
3493
 
2643
3494
  Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](https://code.claude.com/docs/en/hooks#command-hook-fields).
2644
3495
 
2645
- Agent-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.
3496
+ Agent-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](https://code.claude.com/docs/en/hooks#prompt-based-hooks), except `PermissionRequest`.
2646
3497
 
2647
3498
  ### [​](https://code.claude.com/docs/en/hooks\#how-agent-hooks-work) How agent hooks work
2648
3499
 
@@ -2651,22 +3502,13 @@ When an agent hook fires:
2651
3502
  1. Claude Code spawns a subagent with your prompt and the hook’s JSON input
2652
3503
  2. The subagent can use tools like Read, Grep, and Glob to investigate
2653
3504
  3. After up to 50 turns, the subagent returns a structured `{ "ok": true/false }` decision
2654
- 4. Claude Code processes the decision the same way as a prompt hook
3505
+ 4. Claude Code allows the action if `ok` is `true`. If `ok` is `false`, Claude Code handles the block the same way as a prompt hook with `continueOnBlock: true` on that event, as listed under [Response schema](https://code.claude.com/docs/en/hooks#response-schema)
2655
3506
 
2656
3507
  Agent hooks are useful when verification requires inspecting actual files or test output, not just evaluating the hook input data alone.
2657
3508
 
2658
3509
  ### [​](https://code.claude.com/docs/en/hooks\#agent-hook-configuration) Agent hook configuration
2659
3510
 
2660
- Set `type` to `"agent"` and provide a `prompt` string. The configuration fields are the same as [prompt hooks](https://code.claude.com/docs/en/hooks#prompt-hook-configuration), with a longer default timeout:
2661
-
2662
- | Field | Required | Description |
2663
- | --- | --- | --- |
2664
- | `type` | yes | Must be `"agent"` |
2665
- | `prompt` | yes | Prompt describing what to verify. Use `$ARGUMENTS` as a placeholder for the hook input JSON |
2666
- | `model` | no | Model to use. Defaults to a fast model |
2667
- | `timeout` | no | Timeout in seconds. Default: 60 |
2668
-
2669
- The response schema is the same as prompt hooks: `{ "ok": true }` to allow or `{ "ok": false, "reason": "..." }` to block.This `Stop` hook verifies that all unit tests pass before allowing Claude to finish:
3511
+ Set `type` to `"agent"` and provide a `prompt` string, using `$ARGUMENTS` as a placeholder for the hook input JSON. The configuration fields are the same as [prompt hooks](https://code.claude.com/docs/en/hooks#prompt-hook-configuration), except that agent hooks have a longer default timeout of 60 seconds and no `continueOnBlock` field.The response schema is `{ "ok": true }` to allow or `{ "ok": false, "reason": "..." }` to block. On `ok: false`, Claude Code handles an agent hook the way it handles a [prompt hook with `continueOnBlock: true`](https://code.claude.com/docs/en/hooks#response-schema) on the same event; agent hooks have no `continueOnBlock` field, and don’t support the prompt-hook `impossible` field.This `Stop` hook verifies that all unit tests pass before allowing Claude to finish:
2670
3512
 
2671
3513
  ```
2672
3514
  {
@@ -2692,7 +3534,7 @@ By default, hooks block Claude’s execution until they complete. For long-runni
2692
3534
 
2693
3535
  ### [​](https://code.claude.com/docs/en/hooks\#configure-an-async-hook) Configure an async hook
2694
3536
 
2695
- Add `"async": true` to a command hook’s configuration to run it in the background without blocking Claude. This field is only available on `type: "command"` hooks.This hook runs a test script after every `Write` tool call. Claude continues working immediately while `run-tests.sh` executes for up to 120 seconds. When the script finishes, its output is delivered on the next conversation turn:
3537
+ Add `"async": true` to a command hook’s configuration to run it in the background without blocking Claude. This field is only available on `type: "command"` hooks.This hook runs a test script after every `Write` tool call. Claude continues working immediately while `run-tests.sh` executes. When the script finishes, its output is delivered on the next conversation turn:
2696
3538
 
2697
3539
  ```
2698
3540
  {
@@ -2704,8 +3546,7 @@ Add `"async": true` to a command hook’s configuration to run it in the backgro
2704
3546
  {\
2705
3547
  "type": "command",\
2706
3548
  "command": "/path/to/run-tests.sh",\
2707
- "async": true,\
2708
- "timeout": 120\
3549
+ "async": true\
2709
3550
  }\
2710
3551
  ]\
2711
3552
  }\
@@ -2714,11 +3555,19 @@ Add `"async": true` to a command hook’s configuration to run it in the backgro
2714
3555
  }
2715
3556
  ```
2716
3557
 
2717
- The `timeout` field sets the maximum time in seconds for the background process. If not specified, async hooks use the same 10-minute default as sync hooks.
3558
+ Once an async hook is running in the background, Claude Code doesn’t enforce `timeout` on it. Claude Code still enforces `timeout` on a hook you run with `asyncRewake`.Claude Code delivers an async hook’s results only while the session runs:
3559
+
3560
+ - In [non-interactive mode](https://code.claude.com/docs/en/headless) with the `-p` flag, Claude Code kills any async hook still running at teardown and finalizes it with outcome `cancelled`
3561
+ - If your hook’s work must outlive a `claude -p` session, start a fully detached process from it
2718
3562
 
2719
3563
  ### [​](https://code.claude.com/docs/en/hooks\#how-async-hooks-execute) How async hooks execute
2720
3564
 
2721
- When an async hook fires, Claude Code starts the hook process and immediately continues without waiting for it to finish. The hook receives the same JSON input via stdin as a synchronous hook.After the background process exits, if the hook produced a JSON response with an `additionalContext` field, that content is delivered to Claude as context on the next conversation turn. A `systemMessage` field is shown to you, not to Claude.Claude Code validates that JSON response against the same [output schema](https://code.claude.com/docs/en/hooks#json-output) as synchronous hooks, and drops any field whose value has the wrong type, such as a `systemMessage` that isn’t a string, instead of delivering it. Run with `--debug` to see a warning naming each dropped field. Before v2.1.202, malformed JSON output from an async hook could crash the session, and the crash recurred each time the session was resumed.Async hook completion notifications are suppressed by default. To see them, enable verbose mode with `Ctrl+O` or start Claude Code with `--verbose`.
3565
+ When an async hook fires, Claude Code starts the hook process and immediately continues without waiting for it to finish. The hook receives the same JSON input via stdin as a synchronous hook.After the background process exits, Claude Code delivers the `additionalContext` and `systemMessage` fields from the hook’s JSON response to Claude on the next conversation turn. Unlike a synchronous hook’s `systemMessage`, neither field is shown to you.Print the JSON response alone on stdout or on one line of its own:
3566
+
3567
+ - **Alone on stdout**: when the response is the only text on stdout, it can span several lines, such as pretty-printed `jq` output. Spanning several lines requires Claude Code v2.1.295 or later.
3568
+ - **On one line of its own**: an async hook can print other text to stdout when the response fits on one line by itself, for example with `jq -c`.
3569
+
3570
+ Claude Code validates that JSON response against the same [output schema](https://code.claude.com/docs/en/hooks#json-output) as synchronous hooks, and drops any field whose value has the wrong type, such as a `systemMessage` that isn’t a string, instead of delivering it. Run with `--debug` to see a warning naming each dropped field. Before v2.1.202, malformed JSON output from an async hook could crash the session, and the crash recurred each time the session was resumed.Async hook completion notifications are suppressed by default. To see them, enable verbose mode with `Ctrl+O` or start Claude Code with `--verbose`.
2722
3571
 
2723
3572
  ### [​](https://code.claude.com/docs/en/hooks\#run-tests-after-file-changes) Run tests after file changes
2724
3573
 
@@ -2762,8 +3611,7 @@ Then add this configuration to `.claude/settings.json` in your project root. The
2762
3611
  "type": "command",\
2763
3612
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests-async.sh",\
2764
3613
  "args": [],\
2765
- "async": true,\
2766
- "timeout": 300\
3614
+ "async": true\
2767
3615
  }\
2768
3616
  ]\
2769
3617
  }\
@@ -2774,21 +3622,26 @@ Then add this configuration to `.claude/settings.json` in your project root. The
2774
3622
 
2775
3623
  ### [​](https://code.claude.com/docs/en/hooks\#limitations) Limitations
2776
3624
 
2777
- Async hooks have several constraints compared to synchronous hooks:
3625
+ Async hooks have additional constraints compared to synchronous hooks:
2778
3626
 
2779
- - Only `type: "command"` hooks support `async`. Prompt-based hooks can’t run asynchronously.
2780
- - Async hooks can’t block tool calls or return decisions. By the time the hook completes, the triggering action has already proceeded.
2781
3627
  - Hook output is delivered on the next conversation turn. If the session is idle, the response waits until the next user interaction. Exception: an `asyncRewake` hook that exits with code 2 wakes Claude immediately even when the session is idle.
2782
- - Each execution creates a separate background process. There is no deduplication across multiple firings of the same async hook.
3628
+ - Each execution creates a separate background process.
2783
3629
 
2784
3630
  ## [​](https://code.claude.com/docs/en/hooks\#security-considerations) Security considerations
2785
3631
 
2786
3632
  ### [​](https://code.claude.com/docs/en/hooks\#disclaimer) Disclaimer
2787
3633
 
2788
- Command hooks run with your system user’s full permissions.
2789
-
2790
3634
  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.
2791
3635
 
3636
+ ### [​](https://code.claude.com/docs/en/hooks\#workspace-trust) Workspace trust
3637
+
3638
+ Claude Code checks workspace trust before it runs any hook from a settings file. What counts as trusted depends on the session type:
3639
+
3640
+ - **Interactive session**: Claude Code holds back hooks from every settings file, including your own `~/.claude/settings.json`, until you accept the [workspace trust dialog](https://code.claude.com/docs/en/permissions#project-allow-rules-and-workspace-trust) for the folder, or for a parent directory whose trust extends to it
3641
+ - **`-p` or SDK session**: Claude Code never shows the dialog and treats the folder as trusted, so hooks committed in a repository’s `.claude/settings.json` run in a folder you’ve never trusted
3642
+
3643
+ Before you script `claude -p` over a repository you didn’t write, review its `.claude/` settings files, start with [`--bare`](https://code.claude.com/docs/en/headless#start-faster-with-bare-mode), or [turn hooks off for that run](https://code.claude.com/docs/en/hooks#disable-or-remove-hooks) with `--settings '{"disableAllHooks": true}'`. Frontmatter hooks in a project subagent follow a stricter rule than settings-file hooks. [What runs before you trust a folder](https://code.claude.com/docs/en/permissions#what-runs-before-you-trust-a-folder) lists each kind of repository content by session type.
3644
+
2792
3645
  ### [​](https://code.claude.com/docs/en/hooks\#security-best-practices) Security best practices
2793
3646
 
2794
3647
  Keep these practices in mind when writing hooks:
@@ -2801,7 +3654,7 @@ Keep these practices in mind when writing hooks:
2801
3654
 
2802
3655
  ## [​](https://code.claude.com/docs/en/hooks\#windows-powershell-tool) Windows PowerShell tool
2803
3656
 
2804
- On Windows, you can run individual hooks in PowerShell by setting `"shell": "powershell"` on a command hook. Hooks spawn PowerShell directly, so this works regardless of whether `CLAUDE_CODE_USE_POWERSHELL_TOOL` is set. Claude Code auto-detects `pwsh.exe`, the PowerShell 7 and later executable, and falls back to `powershell.exe` for Windows PowerShell 5.1.
3657
+ On Windows, you can run individual hooks in PowerShell by setting `"shell": "powershell"` on a command hook. Claude Code auto-detects `pwsh.exe`, the PowerShell 7 and later executable, and falls back to `powershell.exe` for Windows PowerShell 5.1.
2805
3658
 
2806
3659
  ```
2807
3660
  {
@@ -2822,7 +3675,7 @@ On Windows, you can run individual hooks in PowerShell by setting `"shell": "pow
2822
3675
  }
2823
3676
  ```
2824
3677
 
2825
- To reference the project root from a PowerShell shell-form command, write `${CLAUDE_PROJECT_DIR}` or `$env:CLAUDE_PROJECT_DIR`. As of v2.1.198, Claude Code rewrites the `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_PLUGIN_ROOT}`, and `${CLAUDE_PLUGIN_DATA}` placeholders in a PowerShell shell-form command to PowerShell’s `${env:NAME}` form, whether the hook is defined in `settings.json`, a plugin, or a skill. PowerShell then resolves the value from the exported environment after parsing, so the placeholder works inside double-quoted strings but not inside single-quoted strings, where PowerShell never expands variables.Before v2.1.198, this rewrite applied only to plugin hooks. On earlier versions, a `settings.json` hook needs the `$env:` form or [exec form](https://code.claude.com/docs/en/hooks#exec-form-and-shell-form), where `${CLAUDE_PROJECT_DIR}` is substituted in each `args` element regardless of where the hook is defined.Don’t write the bare `$CLAUDE_PROJECT_DIR` spelling in a PowerShell hook. PowerShell parses it as an undefined local variable and resolves it to `$null`, which leaves the script path without its project-root prefix. Claude Code doesn’t rewrite that form; it logs a warning in the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks) instead.The example below shows a `settings.json` hook that runs a project script with the `$env:` form, which works on every version:
3678
+ To reference the project root from a PowerShell shell-form command, write `${CLAUDE_PROJECT_DIR}` or `$env:CLAUDE_PROJECT_DIR`. Claude Code rewrites the `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_PLUGIN_ROOT}`, and `${CLAUDE_PLUGIN_DATA}` placeholders in a PowerShell shell-form command to PowerShell’s `${env:NAME}` form, whether the hook is defined in `settings.json`, a plugin, or a skill. PowerShell then resolves the value from the exported environment after parsing, so the placeholder works inside double-quoted strings but not inside single-quoted strings, where PowerShell never expands variables.Don’t write the bare `$CLAUDE_PROJECT_DIR` spelling in a PowerShell hook. PowerShell parses it as an undefined local variable and resolves it to `$null`, which leaves the script path without its project-root prefix. Claude Code doesn’t rewrite that form; it logs a warning in the [debug log](https://code.claude.com/docs/en/hooks#debug-hooks) instead.The example below shows a `settings.json` hook that runs a project script with the `$env:` form:
2826
3679
 
2827
3680
  ```
2828
3681
  {
@@ -2834,29 +3687,19 @@ To reference the project root from a PowerShell shell-form command, write `${CLA
2834
3687
 
2835
3688
  ## [​](https://code.claude.com/docs/en/hooks\#debug-hooks) Debug hooks
2836
3689
 
2837
- Hook execution details, including which hooks matched, their exit codes, and full stdout and stderr, are written to the debug log file. Start Claude Code with `claude --debug-file <path>` to write the log to a known location, or run `claude --debug` and read the log at `~/.claude/debug/<session-id>.txt`. The `--debug` flag doesn’t print to the terminal.
3690
+ Hook execution details are written to the debug log file. Start Claude Code with `claude --debug-file <path>` to write the log to a known location, or run `claude --debug` and read the log at `~/.claude/debug/<session-id>.txt`. The `--debug` flag doesn’t print to the terminal.For example, a `PostToolUse` hook on `Write` whose command prints `hook-ran` produces entries like:
2838
3691
 
2839
3692
  ```
2840
- [DEBUG] Executing hooks for PostToolUse:Write
2841
- [DEBUG] Found 1 hook commands to execute
2842
- [DEBUG] Executing hook command: <Your command> with timeout 600000ms
2843
- [DEBUG] Hook command completed with status 0: <Your stdout>
3693
+ 2026-07-19T02:03:24.382Z [DEBUG] Hook output does not start with {, treating as plain text
3694
+ 2026-07-19T02:03:24.382Z [DEBUG] "Hook PostToolUse:Write (PostToolUse) success:\nhook-ran"
2844
3695
  ```
2845
3696
 
2846
- For more granular hook matching details, set `CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose` to see additional log lines such as hook matcher counts and query matching.For troubleshooting common issues like hooks not firing, Stop hooks that keep blocking, or configuration errors, see [Limitations and troubleshooting](https://code.claude.com/docs/en/hooks-guide#limitations-and-troubleshooting) in the guide. For a broader diagnostic walkthrough covering `/context`, `/doctor`, and settings precedence, see [Debug your config](https://code.claude.com/docs/en/debug-your-config).
3697
+ To find a slow hook, search the log for `Hooks:` lines that end in a duration. In Claude Code v2.1.296 or later, each command hook on a tool event, `UserPromptSubmit`, `SessionStart`, `Stop`, and several other events leaves one when it finishes, whatever it printed. The line gives the event name, joined by a colon to the tool name or other value the hook matched on, then the hook’s command in brackets, the plugin it came from if any, how the run ended, and how long it took, as in `Hooks: PostToolUse:Write [.claude/hooks/log-write.sh] finished with status 0 (31ms)`. A run can also end as `timed out after <N>ms`, `cancelled`, `moved to the background`, or `failed to start`. On some events, such as `Notification`, `SessionEnd`, and `PreCompact`, a command hook leaves a `completed with status` line without a duration instead.For more granular hook matching details, set `CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose` to see additional log lines such as hook matcher counts and query matching.For troubleshooting common issues like hooks not firing, Stop hooks that keep blocking, or configuration errors, see [Limitations and troubleshooting](https://code.claude.com/docs/en/hooks-guide#limitations-and-troubleshooting) in the guide. For a broader diagnostic walkthrough covering `/context`, `/doctor`, and settings precedence, see [Debug your config](https://code.claude.com/docs/en/debug-your-config).
2847
3698
 
2848
3699
  Was this page helpful?
2849
3700
 
2850
3701
  YesNo
2851
3702
 
2852
- [Checkpointing](https://code.claude.com/docs/en/checkpointing) [Plugins reference](https://code.claude.com/docs/en/plugins-reference)
2853
-
2854
- Ctrl+I
2855
-
2856
3703
  Assistant
2857
3704
 
2858
- Responses are generated using AI and may contain mistakes.
2859
-
2860
- ![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, and FileChanged as standalone async events, and MessageDisplay as a display-only event that runs while assistant message text streams](https://mintcdn.com/claude-code/uLsR38F1U_5zPppm/images/hooks-lifecycle.svg?w=1100&fit=max&auto=format&n=uLsR38F1U_5zPppm&q=85&s=3fab734aa1c51acc4b37a49d9019d182)
2861
-
2862
- ![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?w=1100&fit=max&auto=format&n=ikqp3_70mqIahteV&q=85&s=12622bb46f39fae9e28e994c0e778399)
3705
+ Responses are generated using AI and may contain mistakes.