1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
1# Hooks reference5# Hooks reference
2 6
3> This page provides reference documentation for implementing hooks in Claude Code.7> Reference for Claude Code hook events, configuration schema, JSON input/output formats, exit codes, async hooks, HTTP hooks, prompt hooks, and MCP tool hooks.
4 8
5<Tip>9<Tip>
6 For a quickstart guide with examples, see [Get started with Claude Code hooks](/en/docs/claude-code/hooks-guide).10 For a quickstart guide with examples, see [Automate workflows with hooks](/en/hooks-guide).
7</Tip>11</Tip>
8 12
9## Configuration13Hooks 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](/en/hooks-guide) instead.
10 14
11Claude Code hooks are configured in your [settings files](/en/docs/claude-code/settings):15## Hook lifecycle
12 16
13* `~/.claude/settings.json` - User settings17Hooks 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. Some events fire once per session, while others fire repeatedly inside the agentic loop:
14* `.claude/settings.json` - Project settings18
15* `.claude/settings.local.json` - Local project settings (not committed)19<div style={{maxWidth: "500px", margin: "0 auto"}}>
16* Enterprise managed policy settings20 <Frame>
17 21 <img src="https://mintcdn.com/claude-code/WLZtXlltXc8aIoIM/images/hooks-lifecycle.svg?fit=max&auto=format&n=WLZtXlltXc8aIoIM&q=85&s=6a0bf67eeb570a96e36b564721fa2a93" alt="Hook lifecycle diagram showing the sequence of hooks from SessionStart through the agentic loop (PreToolUse, PermissionRequest, PostToolUse, SubagentStart/Stop, TaskCreated, TaskCompleted) to Stop or StopFailure, 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, and WorktreeCreate, WorktreeRemove, Notification, ConfigChange, InstructionsLoaded, CwdChanged, and FileChanged as standalone async events" width="520" height="1155" data-path="images/hooks-lifecycle.svg" />
18### Structure22 </Frame>
19 23</div>
20Hooks are organized by matchers, where each matcher can have multiple hooks:24
25The table below summarizes when each event fires. The [Hook events](#hook-events) section documents the full input schema and decision control options for each one.
26
27| Event | When it fires |
28| :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
29| `SessionStart` | When a session begins or resumes |
30| `UserPromptSubmit` | When you submit a prompt, before Claude processes it |
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 |
34| `PostToolUse` | After a tool call succeeds |
35| `PostToolUseFailure` | After a tool call fails |
36| `Notification` | When Claude Code sends a notification |
37| `SubagentStart` | When a subagent is spawned |
38| `SubagentStop` | When a subagent finishes |
39| `TaskCreated` | When a task is being created via `TaskCreate` |
40| `TaskCompleted` | When a task is being marked as completed |
41| `Stop` | When Claude finishes responding |
42| `StopFailure` | When the turn ends due to an API error. Output and exit code are ignored |
43| `TeammateIdle` | When an [agent team](/en/agent-teams) teammate is about to go idle |
44| `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 |
45| `ConfigChange` | When a configuration file changes during a session |
46| `CwdChanged` | When the working directory changes, for example when Claude executes a `cd` command. Useful for reactive environment management with tools like direnv |
47| `FileChanged` | When a watched file changes on disk. The `matcher` field specifies which filenames to watch |
48| `WorktreeCreate` | When a worktree is being created via `--worktree` or `isolation: "worktree"`. Replaces default git behavior |
49| `WorktreeRemove` | When a worktree is being removed, either at session exit or when a subagent finishes |
50| `PreCompact` | Before context compaction |
51| `PostCompact` | After context compaction completes |
52| `Elicitation` | When an MCP server requests user input during a tool call |
53| `ElicitationResult` | After a user responds to an MCP elicitation, before the response is sent back to the server |
54| `SessionEnd` | When a session terminates |
55
56### How a hook resolves
57
58To 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 commands starting with `rm`, so `block-rm.sh` only spawns when both filters match:
21 59
22```json theme={null}60```json theme={null}
23{61{
24 "hooks": {62 "hooks": {
25 "EventName": [63 "PreToolUse": [
26 {64 {
27 "matcher": "ToolPattern",65 "matcher": "Bash",
28 "hooks": [66 "hooks": [
29 {67 {
30 "type": "command",68 "type": "command",
31 "command": "your-command-here"69 "if": "Bash(rm *)",
70 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
32 }71 }
33 ]72 ]
34 }73 }
37}76}
38```77```
39 78
40* **matcher**: Pattern to match tool names, case-sensitive (only applicable for79The script reads the JSON input from stdin, extracts the command, and returns a `permissionDecision` of `"deny"` if it contains `rm -rf`:
41 `PreToolUse` and `PostToolUse`)
42 * Simple strings match exactly: `Write` matches only the Write tool
43 * Supports regex: `Edit|Write` or `Notebook.*`
44 * Use `*` to match all tools. You can also use empty string (`""`) or leave
45 `matcher` blank.
46* **hooks**: Array of hooks to execute when the pattern matches
47 * `type`: Hook execution type - `"command"` for bash commands or `"prompt"` for LLM-based evaluation
48 * `command`: (For `type: "command"`) The bash command to execute (can use `$CLAUDE_PROJECT_DIR` environment variable)
49 * `prompt`: (For `type: "prompt"`) The prompt to send to the LLM for evaluation
50 * `timeout`: (Optional) How long a hook should run, in seconds, before canceling that specific hook
51 80
52For events like `UserPromptSubmit`, `Notification`, `Stop`, and `SubagentStop`81```bash theme={null}
53that don't use matchers, you can omit the matcher field:82#!/bin/bash
83# .claude/hooks/block-rm.sh
84COMMAND=$(jq -r '.tool_input.command')
85
86if echo "$COMMAND" | grep -q 'rm -rf'; then
87 jq -n '{
88 hookSpecificOutput: {
89 hookEventName: "PreToolUse",
90 permissionDecision: "deny",
91 permissionDecisionReason: "Destructive command blocked by hook"
92 }
93 }'
94else
95 exit 0 # allow the command
96fi
97```
54 98
55```json theme={null}99Now suppose Claude Code decides to run `Bash "rm -rf /tmp/build"`. Here's what happens:
56{100
57 "hooks": {101<Frame>
58 "UserPromptSubmit": [102 <img src="https://mintcdn.com/claude-code/-tYw1BD_DEqfyyOZ/images/hook-resolution.svg?fit=max&auto=format&n=-tYw1BD_DEqfyyOZ&q=85&s=c73ebc1eeda2037570427d7af1e0a891" alt="Hook resolution flow: PreToolUse event fires, matcher checks for Bash match, if condition checks for Bash(rm *) match, hook handler runs, result returns to Claude Code" width="930" height="290" data-path="images/hook-resolution.svg" />
59 {103</Frame>
60 "hooks": [104
105<Steps>
106 <Step title="Event fires">
107 The `PreToolUse` event fires. Claude Code sends the tool input as JSON on stdin to the hook:
108
109 ```json theme={null}
110 { "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" }, ... }
111 ```
112 </Step>
113
114 <Step title="Matcher checks">
115 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.
116 </Step>
117
118 <Step title="If condition checks">
119 The `if` condition `"Bash(rm *)"` matches because the command starts with `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.
120 </Step>
121
122 <Step title="Hook handler runs">
123 The script inspects the full command and finds `rm -rf`, so it prints a decision to stdout:
124
125 ```json theme={null}
61 {126 {
62 "type": "command",127 "hookSpecificOutput": {
63 "command": "/path/to/prompt-validator.py"128 "hookEventName": "PreToolUse",
64 }129 "permissionDecision": "deny",
65 ]130 "permissionDecisionReason": "Destructive command blocked by hook"
66 }131 }
67 ]
68 }132 }
69}133 ```
70```
71 134
72### Project-Specific Hook Scripts135 If the command had been a safer `rm` variant like `rm file.txt`, the script would hit `exit 0` instead, which tells Claude Code to allow the tool call with no further action.
136 </Step>
73 137
74You can use the environment variable `CLAUDE_PROJECT_DIR` (only available when138 <Step title="Claude Code acts on the result">
75Claude Code spawns the hook command) to reference scripts stored in your project,139 Claude Code reads the JSON decision, blocks the tool call, and shows Claude the reason.
76ensuring they work regardless of Claude's current directory:140 </Step>
141</Steps>
142
143The [Configuration](#configuration) section below documents the full schema, and each [hook event](#hook-events) section documents what input your command receives and what output it can return.
144
145## Configuration
146
147Hooks are defined in JSON settings files. The configuration has three levels of nesting:
148
1491. Choose a [hook event](#hook-events) to respond to, like `PreToolUse` or `Stop`
1502. Add a [matcher group](#matcher-patterns) to filter when it fires, like "only for the Bash tool"
1513. Define one or more [hook handlers](#hook-handler-fields) to run when matched
152
153See [How a hook resolves](#how-a-hook-resolves) above for a complete walkthrough with an annotated example.
154
155<Note>
156 This page uses specific terms for each level: **hook event** for the lifecycle point, **matcher group** for the filter, and **hook handler** for the shell command, HTTP endpoint, prompt, or agent that runs. "Hook" on its own refers to the general feature.
157</Note>
158
159### Hook locations
160
161Where you define a hook determines its scope:
162
163| Location | Scope | Shareable |
164| :--------------------------------------------------------- | :---------------------------- | :--------------------------------- |
165| `~/.claude/settings.json` | All your projects | No, local to your machine |
166| `.claude/settings.json` | Single project | Yes, can be committed to the repo |
167| `.claude/settings.local.json` | Single project | No, gitignored |
168| Managed policy settings | Organization-wide | Yes, admin-controlled |
169| [Plugin](/en/plugins) `hooks/hooks.json` | When plugin is enabled | Yes, bundled with the plugin |
170| [Skill](/en/skills) or [agent](/en/sub-agents) frontmatter | While the component is active | Yes, defined in the component file |
171
172For details on settings file resolution, see [settings](/en/settings). Enterprise administrators can use `allowManagedHooksOnly` to block user, project, and plugin hooks. See [Hook configuration](/en/settings#hook-configuration).
173
174### Matcher patterns
175
176The `matcher` field is a regex string that filters when hooks fire. Use `"*"`, `""`, or omit `matcher` entirely to match all occurrences. Each event type matches on a different field:
177
178| Event | What the matcher filters | Example matcher values |
179| :------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
180| `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied` | tool name | `Bash`, `Edit\|Write`, `mcp__.*` |
181| `SessionStart` | how the session started | `startup`, `resume`, `clear`, `compact` |
182| `SessionEnd` | why the session ended | `clear`, `resume`, `logout`, `prompt_input_exit`, `bypass_permissions_disabled`, `other` |
183| `Notification` | notification type | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog` |
184| `SubagentStart` | agent type | `Bash`, `Explore`, `Plan`, or custom agent names |
185| `PreCompact`, `PostCompact` | what triggered compaction | `manual`, `auto` |
186| `SubagentStop` | agent type | same values as `SubagentStart` |
187| `ConfigChange` | configuration source | `user_settings`, `project_settings`, `local_settings`, `policy_settings`, `skills` |
188| `CwdChanged` | no matcher support | always fires on every directory change |
189| `FileChanged` | filename (basename of the changed file) | `.envrc`, `.env`, any filename you want to watch |
190| `StopFailure` | error type | `rate_limit`, `authentication_failed`, `billing_error`, `invalid_request`, `server_error`, `max_output_tokens`, `unknown` |
191| `InstructionsLoaded` | load reason | `session_start`, `nested_traversal`, `path_glob_match`, `include`, `compact` |
192| `Elicitation` | MCP server name | your configured MCP server names |
193| `ElicitationResult` | MCP server name | same values as `Elicitation` |
194| `UserPromptSubmit`, `Stop`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove` | no matcher support | always fires on every occurrence |
195
196The matcher is a regex, so `Edit|Write` matches either tool and `Notebook.*` matches any tool starting with Notebook. The matcher runs against a field from the [JSON input](#hook-input-and-output) that Claude Code sends to your hook on stdin. For tool events, that field is `tool_name`. Each [hook event](#hook-events) section lists the full set of matcher values and the input schema for that event.
197
198This example runs a linting script only when Claude writes or edits a file:
77 199
78```json theme={null}200```json theme={null}
79{201{
80 "hooks": {202 "hooks": {
81 "PostToolUse": [203 "PostToolUse": [
82 {204 {
83 "matcher": "Write|Edit",205 "matcher": "Edit|Write",
84 "hooks": [206 "hooks": [
85 {207 {
86 "type": "command",208 "type": "command",
87 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"209 "command": "/path/to/lint-check.sh"
88 }210 }
89 ]211 ]
90 }212 }
93}215}
94```216```
95 217
96### Plugin hooks218`UserPromptSubmit`, `Stop`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove`, 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.
219
220For tool events, you can filter more narrowly by setting the [`if` field](#common-fields) on individual hook handlers. `if` uses [permission rule syntax](/en/permissions) to match against the tool name and arguments together, so `"Bash(git *)"` runs only for `git` commands and `"Edit(*.ts)"` runs only for TypeScript files.
221
222#### Match MCP tools
223
224[MCP](/en/mcp) server tools appear as regular tools in tool events (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest`, `PermissionDenied`), so you can match them the same way you match any other tool name.
225
226MCP tools follow the naming pattern `mcp__<server>__<tool>`, for example:
97 227
98[Plugins](/en/docs/claude-code/plugins) can provide hooks that integrate seamlessly with your user and project hooks. Plugin hooks are automatically merged with your configuration when plugins are enabled.228* `mcp__memory__create_entities`: Memory server's create entities tool
229* `mcp__filesystem__read_file`: Filesystem server's read file tool
230* `mcp__github__search_repositories`: GitHub server's search tool
99 231
100**How plugin hooks work**:232Use regex patterns to target specific MCP tools or groups of tools:
101 233
102* Plugin hooks are defined in the plugin's `hooks/hooks.json` file or in a file given by a custom path to the `hooks` field.234* `mcp__memory__.*` matches all tools from the `memory` server
103* When a plugin is enabled, its hooks are merged with user and project hooks235* `mcp__.*__write.*` matches any tool containing "write" from any server
104* Multiple hooks from different sources can respond to the same event
105* Plugin hooks use the `${CLAUDE_PLUGIN_ROOT}` environment variable to reference plugin files
106 236
107**Example plugin hook configuration**:237This example logs all memory server operations and validates write operations from any MCP server:
108 238
109```json theme={null}239```json theme={null}
110{240{
111 "description": "Automatic code formatting",
112 "hooks": {241 "hooks": {
113 "PostToolUse": [242 "PreToolUse": [
114 {243 {
115 "matcher": "Write|Edit",244 "matcher": "mcp__memory__.*",
116 "hooks": [245 "hooks": [
117 {246 {
118 "type": "command",247 "type": "command",
119 "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",248 "command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
120 "timeout": 30249 }
250 ]
251 },
252 {
253 "matcher": "mcp__.*__write.*",
254 "hooks": [
255 {
256 "type": "command",
257 "command": "/home/user/scripts/validate-mcp-write.py"
121 }258 }
122 ]259 ]
123 }260 }
126}263}
127```264```
128 265
129<Note>266### Hook handler fields
130 Plugin hooks use the same format as regular hooks with an optional `description` field to explain the hook's purpose.
131</Note>
132 267
133<Note>268Each object in the inner `hooks` array is a hook handler: the shell command, HTTP endpoint, LLM prompt, or agent that runs when the matcher matches. There are four types:
134 Plugin hooks run alongside your custom hooks. If multiple hooks match an event, they all execute in parallel.
135</Note>
136 269
137**Environment variables for plugins**:270* **[Command hooks](#command-hook-fields)** (`type: "command"`): run a shell command. Your script receives the event's [JSON input](#hook-input-and-output) on stdin and communicates results back through exit codes and stdout.
271* **[HTTP 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](#json-output) as command hooks.
272* **[Prompt 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](#prompt-based-hooks).
273* **[Agent 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. See [Agent-based hooks](#agent-based-hooks).
138 274
139* `${CLAUDE_PLUGIN_ROOT}`: Absolute path to the plugin directory275#### Common fields
140* `${CLAUDE_PROJECT_DIR}`: Project root directory (same as for project hooks)
141* All standard environment variables are available
142 276
143See the [plugin components reference](/en/docs/claude-code/plugins-reference#hooks) for details on creating plugin hooks.277These fields apply to all hook types:
144 278
145## Prompt-Based Hooks279| Field | Required | Description |
280| :-------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
281| `type` | yes | `"command"`, `"http"`, `"prompt"`, or `"agent"` |
282| `if` | no | Permission rule syntax to filter when this hook runs, such as `"Bash(git *)"` or `"Edit(*.ts)"`. The hook only spawns if the tool call matches the pattern. 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](/en/permissions) |
283| `timeout` | no | Seconds before canceling. Defaults: 600 for command, 30 for prompt, 60 for agent |
284| `statusMessage` | no | Custom spinner message displayed while the hook runs |
285| `once` | no | If `true`, runs only once per session then is removed. Skills only, not agents. See [Hooks in skills and agents](#hooks-in-skills-and-agents) |
146 286
147In addition to bash command hooks (`type: "command"`), Claude Code supports prompt-based hooks (`type: "prompt"`) that use an LLM to evaluate whether to allow or block an action. Prompt-based hooks are currently only supported for `Stop` and `SubagentStop` hooks, where they enable intelligent, context-aware decisions.287#### Command hook fields
148 288
149### How prompt-based hooks work289In addition to the [common fields](#common-fields), command hooks accept these fields:
150 290
151Instead of executing a bash command, prompt-based hooks:291| Field | Required | Description |
292| :-------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
293| `command` | yes | Shell command to execute |
294| `async` | no | If `true`, runs in the background without blocking. See [Run hooks in the background](#run-hooks-in-the-background) |
295| `shell` | no | Shell to use for this hook. Accepts `"bash"` (default) or `"powershell"`. Setting `"powershell"` runs the command via PowerShell on Windows. Does not require `CLAUDE_CODE_USE_POWERSHELL_TOOL` since hooks spawn PowerShell directly |
152 296
1531. Send the hook input and your prompt to a fast LLM (Haiku)297#### HTTP hook fields
1542. The LLM responds with structured JSON containing a decision
1553. Claude Code processes the decision automatically
156 298
157### Configuration299In addition to the [common fields](#common-fields), HTTP hooks accept these fields:
300
301| Field | Required | Description |
302| :--------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
303| `url` | yes | URL to send the POST request to |
304| `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 |
305| `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 |
306
307Claude Code sends the hook's [JSON input](#hook-input-and-output) as the POST request body with `Content-Type: application/json`. The response body uses the same [JSON output format](#json-output) as command hooks.
308
309Error 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"`.
310
311This example sends `PreToolUse` events to a local validation service, authenticating with a token from the `MY_TOKEN` environment variable:
158 312
159```json theme={null}313```json theme={null}
160{314{
161 "hooks": {315 "hooks": {
162 "Stop": [316 "PreToolUse": [
163 {317 {
318 "matcher": "Bash",
164 "hooks": [319 "hooks": [
165 {320 {
166 "type": "prompt",321 "type": "http",
167 "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete."322 "url": "http://localhost:8080/hooks/pre-tool-use",
323 "timeout": 30,
324 "headers": {
325 "Authorization": "Bearer $MY_TOKEN"
326 },
327 "allowedEnvVars": ["MY_TOKEN"]
168 }328 }
169 ]329 ]
170 }330 }
173}333}
174```334```
175 335
176**Fields:**336#### Prompt and agent hook fields
177
178* `type`: Must be `"prompt"`
179* `prompt`: The prompt text to send to the LLM
180 * Use `$ARGUMENTS` as a placeholder for the hook input JSON
181 * If `$ARGUMENTS` is not present, input JSON is appended to the prompt
182* `timeout`: (Optional) Timeout in seconds (default: 30 seconds)
183
184### Response schema
185
186The LLM must respond with JSON containing:
187 337
188```json theme={null}338In addition to the [common fields](#common-fields), prompt and agent hooks accept these fields:
189{
190 "decision": "approve" | "block",
191 "reason": "Explanation for the decision",
192 "continue": false, // Optional: stops Claude entirely
193 "stopReason": "Message shown to user", // Optional: custom stop message
194 "systemMessage": "Warning or context" // Optional: shown to user
195}
196```
197 339
198**Response fields:**340| Field | Required | Description |
341| :------- | :------- | :------------------------------------------------------------------------------------------ |
342| `prompt` | yes | Prompt text to send to the model. Use `$ARGUMENTS` as a placeholder for the hook input JSON |
343| `model` | no | Model to use for evaluation. Defaults to a fast model |
199 344
200* `decision`: `"approve"` allows the action, `"block"` prevents it345All matching hooks run in parallel, and identical handlers are deduplicated automatically. Command hooks are deduplicated by command string, 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.
201* `reason`: Explanation shown to Claude when decision is `"block"`
202* `continue`: (Optional) If `false`, stops Claude's execution entirely
203* `stopReason`: (Optional) Message shown when `continue` is false
204* `systemMessage`: (Optional) Additional message shown to the user
205 346
206### Supported hook events347### Reference scripts by path
207 348
208Prompt-based hooks work with any hook event, but are most useful for:349Use environment variables to reference hook scripts relative to the project or plugin root, regardless of the working directory when the hook runs:
209 350
210* **Stop**: Intelligently decide if Claude should continue working351* `$CLAUDE_PROJECT_DIR`: the project root. Wrap in quotes to handle paths with spaces.
211* **SubagentStop**: Evaluate if a subagent has completed its task352* `${CLAUDE_PLUGIN_ROOT}`: the plugin's installation directory, for scripts bundled with a [plugin](/en/plugins). Changes on each plugin update.
212* **UserPromptSubmit**: Validate user prompts with LLM assistance353* `${CLAUDE_PLUGIN_DATA}`: the plugin's [persistent data directory](/en/plugins-reference#persistent-data-directory), for dependencies and state that should survive plugin updates.
213* **PreToolUse**: Make context-aware permission decisions
214 354
215### Example: Intelligent Stop hook355<Tabs>
356 <Tab title="Project scripts">
357 This example uses `$CLAUDE_PROJECT_DIR` to run a style checker from the project's `.claude/hooks/` directory after any `Write` or `Edit` tool call:
216 358
217```json theme={null}359 ```json theme={null}
218{360 {
219 "hooks": {361 "hooks": {
220 "Stop": [362 "PostToolUse": [
221 {363 {
364 "matcher": "Write|Edit",
222 "hooks": [365 "hooks": [
223 {366 {
224 "type": "prompt",367 "type": "command",
225 "prompt": "You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"decision\": \"approve\" or \"block\", \"reason\": \"your explanation\"}",368 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
226 "timeout": 30
227 }369 }
228 ]370 ]
229 }371 }
230 ]372 ]
231 }373 }
232}374 }
233```375 ```
376 </Tab>
234 377
235### Example: SubagentStop with custom logic378 <Tab title="Plugin scripts">
379 Define plugin hooks in `hooks/hooks.json` with an optional top-level `description` field. When a plugin is enabled, its hooks merge with your user and project hooks.
236 380
237```json theme={null}381 This example runs a formatting script bundled with the plugin:
238{382
383 ```json theme={null}
384 {
385 "description": "Automatic code formatting",
239 "hooks": {386 "hooks": {
240 "SubagentStop": [387 "PostToolUse": [
241 {388 {
389 "matcher": "Write|Edit",
242 "hooks": [390 "hooks": [
243 {391 {
244 "type": "prompt",392 "type": "command",
245 "prompt": "Evaluate if this subagent should stop. Input: $ARGUMENTS\n\nCheck if:\n- The subagent completed its assigned task\n- Any errors occurred that need fixing\n- Additional context gathering is needed\n\nReturn: {\"decision\": \"approve\" or \"block\", \"reason\": \"explanation\"}"393 "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
394 "timeout": 30
246 }395 }
247 ]396 ]
248 }397 }
249 ]398 ]
250 }399 }
251}400 }
401 ```
402
403 See the [plugin components reference](/en/plugins-reference#hooks) for details on creating plugin hooks.
404 </Tab>
405</Tabs>
406
407### Hooks in skills and agents
408
409In addition to settings files and plugins, hooks can be defined directly in [skills](/en/skills) and [subagents](/en/sub-agents) using frontmatter. These hooks are scoped to the component's lifecycle and only run when that component is active.
410
411All hook events are supported. For subagents, `Stop` hooks are automatically converted to `SubagentStop` since that is the event that fires when a subagent completes.
412
413Hooks use the same configuration format as settings-based hooks but are scoped to the component's lifetime and cleaned up when it finishes.
414
415This skill defines a `PreToolUse` hook that runs a security validation script before each `Bash` command:
416
417```yaml theme={null}
418---
419name: secure-operations
420description: Perform operations with security checks
421hooks:
422 PreToolUse:
423 - matcher: "Bash"
424 hooks:
425 - type: command
426 command: "./scripts/security-check.sh"
427---
252```428```
253 429
254### Comparison with bash command hooks430Agents use the same format in their YAML frontmatter.
255 431
256| Feature | Bash Command Hooks | Prompt-Based Hooks |432### The `/hooks` menu
257| --------------------- | ----------------------- | ------------------------------ |
258| **Execution** | Runs bash script | Queries LLM |
259| **Decision logic** | You implement in code | LLM evaluates context |
260| **Setup complexity** | Requires script file | Just configure prompt |
261| **Context awareness** | Limited to script logic | Natural language understanding |
262| **Performance** | Fast (local execution) | Slower (API call) |
263| **Use case** | Deterministic rules | Context-aware decisions |
264 433
265### Best practices434Type `/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.
266 435
267* **Be specific in prompts**: Clearly state what you want the LLM to evaluate436The menu displays all four hook types: `command`, `prompt`, `agent`, and `http`. Each hook is labeled with a `[type]` prefix and a source indicating where it was defined:
268* **Include decision criteria**: List the factors the LLM should consider
269* **Test your prompts**: Verify the LLM makes correct decisions for your use cases
270* **Set appropriate timeouts**: Default is 30 seconds, adjust if needed
271* **Use for complex decisions**: Bash hooks are better for simple, deterministic rules
272 437
273See the [plugin components reference](/en/docs/claude-code/plugins-reference#hooks) for details on creating plugin hooks.438* `User`: from `~/.claude/settings.json`
439* `Project`: from `.claude/settings.json`
440* `Local`: from `.claude/settings.local.json`
441* `Plugin`: from a plugin's `hooks/hooks.json`
442* `Session`: registered in memory for the current session
443* `Built-in`: registered internally by Claude Code
274 444
275## Hook Events445Selecting 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.
276 446
277### PreToolUse447### Disable or remove hooks
278 448
279Runs after Claude creates tool parameters and before processing the tool call.449To remove a hook, delete its entry from the settings JSON file.
280 450
281**Common matchers:**451To 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.
282 452
283* `Task` - Subagent tasks (see [subagents documentation](/en/docs/claude-code/sub-agents))453The `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 cannot disable those managed hooks. Only `disableAllHooks` set at the managed settings level can disable managed hooks.
284* `Bash` - Shell commands
285* `Glob` - File pattern matching
286* `Grep` - Content search
287* `Read` - File reading
288* `Edit` - File editing
289* `Write` - File writing
290* `WebFetch`, `WebSearch` - Web operations
291 454
292### PostToolUse455Direct edits to hooks in settings files are normally picked up automatically by the file watcher.
293 456
294Runs immediately after a tool completes successfully.457## Hook input and output
295 458
296Recognizes the same matcher values as PreToolUse.459Command 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](#hook-events) includes its specific input schema and decision control options.
297 460
298### PostCustomToolCall461### Common input fields
299 462
300Runs after an MCP tool completes but **before** `PostToolUse` hooks execute. This hook allows you to modify the tool's output before it's processed further or shown to the model.463Hook events receive these fields as JSON, in addition to event-specific fields documented in each [hook event](#hook-events) section. For command hooks, this JSON arrives via stdin. For HTTP hooks, it arrives as the POST request body.
301 464
302**Key characteristics:**465| Field | Description |
466| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
467| `session_id` | Current session identifier |
468| `transcript_path` | Path to conversation JSON |
469| `cwd` | Current working directory when the hook is invoked |
470| `permission_mode` | Current [permission mode](/en/permissions#permission-modes): `"default"`, `"plan"`, `"acceptEdits"`, `"auto"`, `"dontAsk"`, or `"bypassPermissions"`. Not all events receive this field: see each event's JSON example below to check |
471| `hook_event_name` | Name of the event that fired |
303 472
304* **Only runs for MCP tools** (tools starting with `mcp__`)473When running with `--agent` or inside a subagent, two additional fields are included:
305* Executes after the tool completes but before `PostToolUse` hooks
306* Can modify tool output using the `updatedOutput` field
307* Original tool response is replaced with modified output
308 474
309**Common use cases:**475| Field | Description |
476| :----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
477| `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. |
478| `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. |
310 479
311* Adding metadata to MCP tool responses480For example, a `PreToolUse` hook for a Bash command receives this on stdin:
312* Filtering sensitive information from tool outputs
313* Transforming response formats
314* Logging MCP tool usage with enriched data
315 481
316**Important**: If multiple hooks provide `updatedOutput` for the same tool call, they may conflict. Hook execution order is non-deterministic when hooks run in parallel. Only configure one hook per tool to modify output.482```json theme={null}
483{
484 "session_id": "abc123",
485 "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
486 "cwd": "/home/user/my-project",
487 "permission_mode": "default",
488 "hook_event_name": "PreToolUse",
489 "tool_name": "Bash",
490 "tool_input": {
491 "command": "npm test"
492 }
493}
494```
317 495
318**Matchers:**496The `tool_name` and `tool_input` fields are event-specific. Each [hook event](#hook-events) section documents the additional fields for that event.
319Recognizes the same matcher values as PreToolUse, but will only execute for MCP tools (`mcp__*`).
320 497
321### Notification498### Exit code output
322 499
323Runs when Claude Code sends notifications. Notifications are sent when:500The exit code from your hook command tells Claude Code whether the action should proceed, be blocked, or be ignored.
324 501
3251. Claude needs your permission to use a tool. Example: "Claude needs your502**Exit 0** means success. Claude Code parses stdout for [JSON output fields](#json-output). JSON output is only processed on exit 0. For most events, stdout is only shown in verbose mode (`Ctrl+O`). The exceptions are `UserPromptSubmit` and `SessionStart`, where stdout is added as context that Claude can see and act on.
326 permission to use Bash"
3272. The prompt input has been idle for at least 60 seconds. "Claude is waiting
328 for your input"
329 503
330### UserPromptSubmit504**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](#exit-code-2-behavior-per-event) for the full list.
331 505
332Runs when the user submits a prompt, before Claude processes it. This allows you506**Any other exit code** is a non-blocking error. stderr is shown in verbose mode (`Ctrl+O`) and execution continues.
333to add additional context based on the prompt/conversation, validate prompts, or
334block certain types of prompts.
335 507
336### Stop508For example, a hook command script that blocks dangerous Bash commands:
337 509
338Runs when the main Claude Code agent has finished responding. Does not run if510```bash theme={null}
339the stoppage occurred due to a user interrupt.511#!/bin/bash
512# Reads JSON input from stdin, checks the command
513command=$(jq -r '.tool_input.command' < /dev/stdin)
340 514
341### SubagentStop515if [[ "$command" == rm* ]]; then
516 echo "Blocked: rm commands are not allowed" >&2
517 exit 2 # Blocking error: tool call is prevented
518fi
342 519
343Runs when a Claude Code subagent (Task tool call) has finished responding.520exit 0 # Success: tool call proceeds
521```
344 522
345### PreCompact523#### Exit code 2 behavior per event
524
525Exit code 2 is the way a hook signals "stop, don't do this." The effect depends on the event, because some events represent actions that can be blocked (like a tool call that hasn't happened yet) and others represent things that already happened or can't be prevented.
526
527| Hook event | Can block? | What happens on exit 2 |
528| :------------------- | :--------- | :----------------------------------------------------------------------------------------------------------------------------------- |
529| `PreToolUse` | Yes | Blocks the tool call |
530| `PermissionRequest` | Yes | Denies the permission |
531| `UserPromptSubmit` | Yes | Blocks prompt processing and erases the prompt |
532| `Stop` | Yes | Prevents Claude from stopping, continues the conversation |
533| `SubagentStop` | Yes | Prevents the subagent from stopping |
534| `TeammateIdle` | Yes | Prevents the teammate from going idle (teammate continues working) |
535| `TaskCreated` | Yes | Rolls back the task creation |
536| `TaskCompleted` | Yes | Prevents the task from being marked as completed |
537| `ConfigChange` | Yes | Blocks the configuration change from taking effect (except `policy_settings`) |
538| `StopFailure` | No | Output and exit code are ignored |
539| `PostToolUse` | No | Shows stderr to Claude (tool already ran) |
540| `PostToolUseFailure` | No | Shows stderr to Claude (tool already failed) |
541| `PermissionDenied` | No | Exit code and stderr are ignored (denial already occurred). Use JSON `hookSpecificOutput.retry: true` to tell the model it may retry |
542| `Notification` | No | Shows stderr to user only |
543| `SubagentStart` | No | Shows stderr to user only |
544| `SessionStart` | No | Shows stderr to user only |
545| `SessionEnd` | No | Shows stderr to user only |
546| `CwdChanged` | No | Shows stderr to user only |
547| `FileChanged` | No | Shows stderr to user only |
548| `PreCompact` | No | Shows stderr to user only |
549| `PostCompact` | No | Shows stderr to user only |
550| `Elicitation` | Yes | Denies the elicitation |
551| `ElicitationResult` | Yes | Blocks the response (action becomes decline) |
552| `WorktreeCreate` | Yes | Any non-zero exit code causes worktree creation to fail |
553| `WorktreeRemove` | No | Failures are logged in debug mode only |
554| `InstructionsLoaded` | No | Exit code is ignored |
555
556### HTTP response handling
557
558HTTP hooks use HTTP status codes and response bodies instead of exit codes and stdout:
559
560* **2xx with an empty body**: success, equivalent to exit code 0 with no output
561* **2xx with a plain text body**: success, the text is added as context
562* **2xx with a JSON body**: success, parsed using the same [JSON output](#json-output) schema as command hooks
563* **Non-2xx status**: non-blocking error, execution continues
564* **Connection failure or timeout**: non-blocking error, execution continues
565
566Unlike command hooks, HTTP hooks cannot 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.
567
568### JSON output
569
570Exit codes let you allow or block, but JSON output gives you finer-grained control. Instead of exiting with code 2 to block, exit 0 and print a JSON object to stdout. Claude Code reads specific fields from that JSON to control behavior, including [decision control](#decision-control) for blocking, allowing, or escalating to the user.
346 571
347Runs before Claude Code is about to run a compact operation.572<Note>
573 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.
574</Note>
575
576Your 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](/en/hooks-guide#json-validation-failed) in the troubleshooting guide.
577
578Hook output injected into context (`additionalContext`, `systemMessage`, or plain stdout) is 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.
579
580The JSON object supports three kinds of fields:
581
582* **Universal fields** like `continue` work across all events. These are listed in the table below.
583* **Top-level `decision` and `reason`** are used by some events to block or provide feedback.
584* **`hookSpecificOutput`** is a nested object for events that need richer control. It requires a `hookEventName` field set to the event name.
585
586| Field | Default | Description |
587| :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------- |
588| `continue` | `true` | If `false`, Claude stops processing entirely after the hook runs. Takes precedence over any event-specific decision fields |
589| `stopReason` | none | Message shown to the user when `continue` is `false`. Not shown to Claude |
590| `suppressOutput` | `false` | If `true`, hides stdout from verbose mode output |
591| `systemMessage` | none | Warning message shown to the user |
592
593To stop Claude entirely regardless of event type:
594
595```json theme={null}
596{ "continue": false, "stopReason": "Build failed, fix errors before continuing" }
597```
598
599#### Decision control
600
601Not every event supports blocking or controlling behavior through JSON. The events that do each use a different set of fields to express that decision. Use this table as a quick reference before writing a hook:
602
603| Events | Decision pattern | Key fields |
604| :-------------------------------------------------------------------------------------------------------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
605| UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop, ConfigChange | Top-level `decision` | `decision: "block"`, `reason` |
606| 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 |
607| PreToolUse | `hookSpecificOutput` | `permissionDecision` (allow/deny/ask/defer), `permissionDecisionReason` |
608| PermissionRequest | `hookSpecificOutput` | `decision.behavior` (allow/deny) |
609| PermissionDenied | `hookSpecificOutput` | `retry: true` tells the model it may retry the denied tool call |
610| WorktreeCreate | path return | Command hook prints path on stdout; HTTP hook returns `hookSpecificOutput.worktreePath`. Hook failure or missing path fails creation |
611| Elicitation | `hookSpecificOutput` | `action` (accept/decline/cancel), `content` (form field values for accept) |
612| ElicitationResult | `hookSpecificOutput` | `action` (accept/decline/cancel), `content` (form field values override) |
613| WorktreeRemove, Notification, SessionEnd, PreCompact, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, FileChanged | None | No decision control. Used for side effects like logging or cleanup |
614
615Here are examples of each pattern in action:
616
617<Tabs>
618 <Tab title="Top-level decision">
619 Used by `UserPromptSubmit`, `PostToolUse`, `PostToolUseFailure`, `Stop`, `SubagentStop`, and `ConfigChange`. The only value is `"block"`. To allow the action to proceed, omit `decision` from your JSON, or exit 0 without any JSON at all:
620
621 ```json theme={null}
622 {
623 "decision": "block",
624 "reason": "Test suite must pass before proceeding"
625 }
626 ```
627 </Tab>
628
629 <Tab title="PreToolUse">
630 Uses `hookSpecificOutput` for richer control: allow, deny, or escalate to the user. You can also modify tool input before it runs or inject additional context for Claude. See [PreToolUse decision control](#pretooluse-decision-control) for the full set of options.
631
632 ```json theme={null}
633 {
634 "hookSpecificOutput": {
635 "hookEventName": "PreToolUse",
636 "permissionDecision": "deny",
637 "permissionDecisionReason": "Database writes are not allowed"
638 }
639 }
640 ```
641 </Tab>
642
643 <Tab title="PermissionRequest">
644 Uses `hookSpecificOutput` to allow or deny a permission request on behalf of the user. When allowing, you can also modify the tool's input or apply permission rules so the user isn't prompted again. See [PermissionRequest decision control](#permissionrequest-decision-control) for the full set of options.
645
646 ```json theme={null}
647 {
648 "hookSpecificOutput": {
649 "hookEventName": "PermissionRequest",
650 "decision": {
651 "behavior": "allow",
652 "updatedInput": {
653 "command": "npm run lint"
654 }
655 }
656 }
657 }
658 ```
659 </Tab>
660</Tabs>
661
662For extended examples including Bash command validation, prompt filtering, and auto-approval scripts, see [What you can automate](/en/hooks-guide#what-you-can-automate) in the guide and the [Bash command validator reference implementation](https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py).
348 663
349**Matchers:**664## Hook events
350 665
351* `manual` - Invoked from `/compact`666Each event corresponds to a point in Claude Code's lifecycle where hooks can run. The sections below are ordered to match the lifecycle: from session setup through the agentic loop to session end. Each section describes when the event fires, what matchers it supports, the JSON input it receives, and how to control behavior through output.
352* `auto` - Invoked from auto-compact (due to full context window)
353 667
354### SessionStart668### SessionStart
355 669
356Runs when Claude Code starts a new session or resumes an existing session (which670Runs 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 does not require a script, use [CLAUDE.md](/en/memory) instead.
357currently does start a new session under the hood). Useful for loading in671
358development context like existing issues or recent changes to your codebase, installing dependencies, or setting up environment variables.672SessionStart runs on every session, so keep these hooks fast. Only `type: "command"` hooks are supported.
673
674The matcher value corresponds to how the session was initiated:
675
676| Matcher | When it fires |
677| :-------- | :------------------------------------- |
678| `startup` | New session |
679| `resume` | `--resume`, `--continue`, or `/resume` |
680| `clear` | `/clear` |
681| `compact` | Auto or manual compaction |
682
683#### SessionStart input
684
685In addition to the [common input fields](#common-input-fields), SessionStart hooks receive `source`, `model`, and optionally `agent_type`. The `source` field indicates how the session started: `"startup"` for new sessions, `"resume"` for resumed sessions, `"clear"` after `/clear`, or `"compact"` after compaction. The `model` field contains the model identifier. If you start Claude Code with `claude --agent <name>`, an `agent_type` field contains the agent name.
686
687```json theme={null}
688{
689 "session_id": "abc123",
690 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
691 "cwd": "/Users/...",
692 "hook_event_name": "SessionStart",
693 "source": "startup",
694 "model": "claude-sonnet-4-6"
695}
696```
697
698#### SessionStart decision control
699
700Any text your hook script prints to stdout is added as context for Claude. In addition to the [JSON output fields](#json-output) available to all hooks, you can return these event-specific fields:
359 701
360**Matchers:**702| Field | Description |
703| :------------------ | :------------------------------------------------------------------------ |
704| `additionalContext` | String added to Claude's context. Multiple hooks' values are concatenated |
361 705
362* `startup` - Invoked from startup706```json theme={null}
363* `resume` - Invoked from `--resume`, `--continue`, or `/resume`707{
364* `clear` - Invoked from `/clear`708 "hookSpecificOutput": {
365* `compact` - Invoked from auto or manual compact.709 "hookEventName": "SessionStart",
710 "additionalContext": "My additional context here"
711 }
712}
713```
366 714
367#### Persisting environment variables715#### Persist environment variables
368 716
369SessionStart 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.717SessionStart 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.
370 718
371**Example: Setting individual environment variables**719To set individual environment variables, write `export` statements to `CLAUDE_ENV_FILE`. Use append (`>>`) to preserve variables set by other hooks:
372 720
373```bash theme={null}721```bash theme={null}
374#!/bin/bash722#!/bin/bash
375 723
376if [ -n "$CLAUDE_ENV_FILE" ]; then724if [ -n "$CLAUDE_ENV_FILE" ]; then
377 echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"725 echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
378 echo 'export API_KEY=your-api-key' >> "$CLAUDE_ENV_FILE"726 echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
379 echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"727 echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
380fi728fi
381 729
382exit 0730exit 0
383```731```
384 732
385**Example: Persisting all environment changes from the hook**733To capture all environment changes from setup commands, compare the exported variables before and after:
386
387When your setup modifies the environment (e.g., `nvm use`), capture and persist all changes by diffing the environment:
388 734
389```bash theme={null}735```bash theme={null}
390#!/bin/bash736#!/bin/bash
403exit 0749exit 0
404```750```
405 751
406Any variables written to this file will be available in all subsequent bash commands that Claude Code executes during the session.752Any variables written to this file will be available in all subsequent Bash commands that Claude Code executes during the session.
407 753
408<Note>754<Note>
409 `CLAUDE_ENV_FILE` is only available for SessionStart hooks. Other hook types do not have access to this variable.755 `CLAUDE_ENV_FILE` is available for SessionStart, [CwdChanged](#cwdchanged), and [FileChanged](#filechanged) hooks. Other hook types do not have access to this variable.
410</Note>756</Note>
411 757
412### SessionEnd758### InstructionsLoaded
413 759
414Runs when a Claude Code session ends. Useful for cleanup tasks, logging session760Fires 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 does not support blocking or decision control. It runs asynchronously for observability purposes.
415statistics, or saving session state.
416 761
417The `reason` field in the hook input will be one of:762The 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.
418 763
419* `clear` - Session cleared with /clear command764#### InstructionsLoaded input
420* `logout` - User logged out
421* `prompt_input_exit` - User exited while prompt input was visible
422* `other` - Other exit reasons
423 765
424## Hook Input766In addition to the [common input fields](#common-input-fields), InstructionsLoaded hooks receive these fields:
425 767
426Hooks receive JSON data via stdin containing session information and768| Field | Description |
427event-specific data:769| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
770| `file_path` | Absolute path to the instruction file that was loaded |
771| `memory_type` | Scope of the file: `"User"`, `"Project"`, `"Local"`, or `"Managed"` |
772| `load_reason` | Why the file was loaded: `"session_start"`, `"nested_traversal"`, `"path_glob_match"`, `"include"`, or `"compact"`. The `"compact"` value fires when instruction files are re-loaded after a compaction event |
773| `globs` | Path glob patterns from the file's `paths:` frontmatter, if any. Present only for `path_glob_match` loads |
774| `trigger_file_path` | Path to the file whose access triggered this load, for lazy loads |
775| `parent_file_path` | Path to the parent instruction file that included this one, for `include` loads |
428 776
429```typescript theme={null}777```json theme={null}
430{778{
431 // Common fields779 "session_id": "abc123",
432 session_id: string780 "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
433 transcript_path: string // Path to conversation JSON781 "cwd": "/Users/my-project",
434 cwd: string // The current working directory when the hook is invoked782 "hook_event_name": "InstructionsLoaded",
435 permission_mode: string // Current permission mode: "default", "plan", "acceptEdits", or "bypassPermissions"783 "file_path": "/Users/my-project/CLAUDE.md",
436 784 "memory_type": "Project",
437 // Event-specific fields785 "load_reason": "session_start"
438 hook_event_name: string
439 ...
440}786}
441```787```
442 788
443### PreToolUse Input789#### InstructionsLoaded decision control
444 790
445The exact schema for `tool_input` depends on the tool.791InstructionsLoaded hooks have no decision control. They cannot block or modify instruction loading. Use this event for audit logging, compliance tracking, or observability.
446 792
447```json theme={null}793### UserPromptSubmit
448{794
449 "session_id": "abc123",795Runs when the user submits a prompt, before Claude processes it. This allows you
450 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",796to add additional context based on the prompt/conversation, validate prompts, or
451 "cwd": "/Users/...",797block certain types of prompts.
452 "permission_mode": "default",
453 "hook_event_name": "PreToolUse",
454 "tool_name": "Write",
455 "tool_input": {
456 "file_path": "/path/to/file.txt",
457 "content": "file content"
458 }
459}
460```
461 798
462### PostToolUse Input799#### UserPromptSubmit input
463 800
464The exact schema for `tool_input` and `tool_response` depends on the tool.801In addition to the [common input fields](#common-input-fields), UserPromptSubmit hooks receive the `prompt` field containing the text the user submitted.
465 802
466```json theme={null}803```json theme={null}
467{804{
469 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",806 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
470 "cwd": "/Users/...",807 "cwd": "/Users/...",
471 "permission_mode": "default",808 "permission_mode": "default",
472 "hook_event_name": "PostToolUse",809 "hook_event_name": "UserPromptSubmit",
473 "tool_name": "Write",810 "prompt": "Write a function to calculate the factorial of a number"
474 "tool_input": {
475 "file_path": "/path/to/file.txt",
476 "content": "file content"
477 },
478 "tool_response": {
479 "filePath": "/path/to/file.txt",
480 "success": true
481 }
482}811}
483```812```
484 813
485### PostCustomToolCall Input814#### UserPromptSubmit decision control
486 815
487The exact schema for `tool_input` and `tool_response` depends on the MCP tool.816`UserPromptSubmit` hooks can control whether a user prompt is processed and add context. All [JSON output fields](#json-output) are available.
488 817
489```json theme={null}818There are two ways to add context to the conversation on exit code 0:
490{819
820* **Plain text stdout**: any non-JSON text written to stdout is added as context
821* **JSON with `additionalContext`**: use the JSON format below for more control. The `additionalContext` field is added as context
822
823Plain stdout is shown as hook output in the transcript. The `additionalContext` field is added more discretely.
824
825To block a prompt, return a JSON object with `decision` set to `"block"`:
826
827| Field | Description |
828| :------------------ | :----------------------------------------------------------------------------------------------------------------- |
829| `decision` | `"block"` prevents the prompt from being processed and erases it from context. Omit to allow the prompt to proceed |
830| `reason` | Shown to the user when `decision` is `"block"`. Not added to context |
831| `additionalContext` | String added to Claude's context |
832
833```json theme={null}
834{
835 "decision": "block",
836 "reason": "Explanation for decision",
837 "hookSpecificOutput": {
838 "hookEventName": "UserPromptSubmit",
839 "additionalContext": "My additional context here"
840 }
841}
842```
843
844<Note>
845 The JSON format isn't required for simple use cases. To add context, you can print plain text to stdout with exit code 0. Use JSON when you need to
846 block prompts or want more structured control.
847</Note>
848
849### PreToolUse
850
851Runs 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](#match-mcp-tools).
852
853Use [PreToolUse decision control](#pretooluse-decision-control) to allow, deny, ask, or defer the tool call.
854
855#### PreToolUse input
856
857In addition to the [common input fields](#common-input-fields), PreToolUse hooks receive `tool_name`, `tool_input`, and `tool_use_id`. The `tool_input` fields depend on the tool:
858
859##### Bash
860
861Executes shell commands.
862
863| Field | Type | Example | Description |
864| :------------------ | :------ | :----------------- | :-------------------------------------------- |
865| `command` | string | `"npm test"` | The shell command to execute |
866| `description` | string | `"Run test suite"` | Optional description of what the command does |
867| `timeout` | number | `120000` | Optional timeout in milliseconds |
868| `run_in_background` | boolean | `false` | Whether to run the command in background |
869
870##### Write
871
872Creates or overwrites a file.
873
874| Field | Type | Example | Description |
875| :---------- | :----- | :-------------------- | :--------------------------------- |
876| `file_path` | string | `"/path/to/file.txt"` | Absolute path to the file to write |
877| `content` | string | `"file content"` | Content to write to the file |
878
879##### Edit
880
881Replaces a string in an existing file.
882
883| Field | Type | Example | Description |
884| :------------ | :------ | :-------------------- | :--------------------------------- |
885| `file_path` | string | `"/path/to/file.txt"` | Absolute path to the file to edit |
886| `old_string` | string | `"original text"` | Text to find and replace |
887| `new_string` | string | `"replacement text"` | Replacement text |
888| `replace_all` | boolean | `false` | Whether to replace all occurrences |
889
890##### Read
891
892Reads file contents.
893
894| Field | Type | Example | Description |
895| :---------- | :----- | :-------------------- | :----------------------------------------- |
896| `file_path` | string | `"/path/to/file.txt"` | Absolute path to the file to read |
897| `offset` | number | `10` | Optional line number to start reading from |
898| `limit` | number | `50` | Optional number of lines to read |
899
900##### Glob
901
902Finds files matching a glob pattern.
903
904| Field | Type | Example | Description |
905| :-------- | :----- | :--------------- | :--------------------------------------------------------------------- |
906| `pattern` | string | `"**/*.ts"` | Glob pattern to match files against |
907| `path` | string | `"/path/to/dir"` | Optional directory to search in. Defaults to current working directory |
908
909##### Grep
910
911Searches file contents with regular expressions.
912
913| Field | Type | Example | Description |
914| :------------ | :------ | :--------------- | :------------------------------------------------------------------------------------ |
915| `pattern` | string | `"TODO.*fix"` | Regular expression pattern to search for |
916| `path` | string | `"/path/to/dir"` | Optional file or directory to search in |
917| `glob` | string | `"*.ts"` | Optional glob pattern to filter files |
918| `output_mode` | string | `"content"` | `"content"`, `"files_with_matches"`, or `"count"`. Defaults to `"files_with_matches"` |
919| `-i` | boolean | `true` | Case insensitive search |
920| `multiline` | boolean | `false` | Enable multiline matching |
921
922##### WebFetch
923
924Fetches and processes web content.
925
926| Field | Type | Example | Description |
927| :------- | :----- | :---------------------------- | :----------------------------------- |
928| `url` | string | `"https://example.com/api"` | URL to fetch content from |
929| `prompt` | string | `"Extract the API endpoints"` | Prompt to run on the fetched content |
930
931##### WebSearch
932
933Searches the web.
934
935| Field | Type | Example | Description |
936| :---------------- | :----- | :----------------------------- | :------------------------------------------------ |
937| `query` | string | `"react hooks best practices"` | Search query |
938| `allowed_domains` | array | `["docs.example.com"]` | Optional: only include results from these domains |
939| `blocked_domains` | array | `["spam.example.com"]` | Optional: exclude results from these domains |
940
941##### Agent
942
943Spawns a [subagent](/en/sub-agents).
944
945| Field | Type | Example | Description |
946| :-------------- | :----- | :------------------------- | :------------------------------------------- |
947| `prompt` | string | `"Find all API endpoints"` | The task for the agent to perform |
948| `description` | string | `"Find API endpoints"` | Short description of the task |
949| `subagent_type` | string | `"Explore"` | Type of specialized agent to use |
950| `model` | string | `"sonnet"` | Optional model alias to override the default |
951
952##### AskUserQuestion
953
954Asks the user one to four multiple-choice questions.
955
956| Field | Type | Example | Description |
957| :---------- | :----- | :----------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
958| `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 |
959| `answers` | object | `{"Which framework?": "React"}` | Optional. Maps question text to the selected option label. Multi-select answers join labels with commas. Claude does not set this field; supply it via `updatedInput` to answer programmatically |
960
961#### PreToolUse decision control
962
963`PreToolUse` hooks can control whether a tool call proceeds. Unlike other hooks that use a top-level `decision` field, PreToolUse returns its decision inside a `hookSpecificOutput` object. This gives it richer control: four outcomes (allow, deny, ask, or defer) plus the ability to modify tool input before execution.
964
965| Field | Description |
966| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
967| `permissionDecision` | `"allow"` skips the permission prompt. `"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](/en/permissions#manage-permissions) still apply when a hook returns `"allow"` |
968| `permissionDecisionReason` | For `"allow"` and `"ask"`, shown to the user but not Claude. For `"deny"`, shown to Claude. For `"defer"`, ignored |
969| `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 |
970| `additionalContext` | String added to Claude's context before the tool executes. For `"defer"`, ignored |
971
972When multiple PreToolUse hooks return different decisions, precedence is `deny` > `defer` > `ask` > `allow`.
973
974When 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.
975
976```json theme={null}
977{
978 "hookSpecificOutput": {
979 "hookEventName": "PreToolUse",
980 "permissionDecision": "allow",
981 "permissionDecisionReason": "My reason here",
982 "updatedInput": {
983 "field_to_modify": "new value"
984 },
985 "additionalContext": "Current environment: production. Proceed with caution."
986 }
987}
988```
989
990`AskUserQuestion` and `ExitPlanMode` require user interaction and normally block in [non-interactive mode](/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`](#askuserquestion) object mapping each question's text to the chosen answer.
991
992<Note>
993 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.
994</Note>
995
996#### Defer a tool call for later
997
998`"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](/en/headless) with the `-p` flag. In interactive sessions it logs a warning and ignores the hook result.
999
1000<Note>
1001 The `defer` value requires Claude Code v2.1.89 or later. Earlier versions do not recognize it and the tool proceeds through the normal permission flow.
1002</Note>
1003
1004The `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:
1005
10061. Claude calls `AskUserQuestion`. The `PreToolUse` hook fires.
10072. The hook returns `permissionDecision: "defer"`. The tool does not execute. The process exits with `stop_reason: "tool_deferred"` and the pending tool call preserved in the transcript.
10083. The calling process reads `deferred_tool_use` from the SDK result, surfaces the question in its own UI, and waits for an answer.
10094. The calling process runs `claude -p --resume <session-id>`. The same tool call fires `PreToolUse` again.
10105. The hook returns `permissionDecision: "allow"` with the answer in `updatedInput`. The tool executes and Claude continues.
1011
1012The `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:
1013
1014```json theme={null}
1015{
1016 "type": "result",
1017 "subtype": "success",
1018 "stop_reason": "tool_deferred",
1019 "session_id": "abc123",
1020 "deferred_tool_use": {
1021 "id": "toolu_01abc",
1022 "name": "AskUserQuestion",
1023 "input": { "questions": [{ "question": "Which framework?", "header": "Framework", "options": [{"label": "React"}, {"label": "Vue"}], "multiSelect": false }] }
1024 }
1025}
1026```
1027
1028There is no timeout or retry limit. The session remains on disk until you resume it. 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.
1029
1030`"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.
1031
1032If 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.
1033
1034<Warning>
1035 `--resume` does not restore the permission mode from the prior session. Pass the same `--permission-mode` flag on resume that was active when the tool was deferred. Claude Code logs a warning if the modes differ.
1036</Warning>
1037
1038### PermissionRequest
1039
1040Runs when the user is shown a permission dialog.
1041Use [PermissionRequest decision control](#permissionrequest-decision-control) to allow or deny on behalf of the user.
1042
1043Matches on tool name, same values as PreToolUse.
1044
1045#### PermissionRequest input
1046
1047PermissionRequest 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.
1048
1049```json theme={null}
1050{
491 "session_id": "abc123",1051 "session_id": "abc123",
492 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",1052 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
493 "cwd": "/Users/...",1053 "cwd": "/Users/...",
494 "permission_mode": "default",1054 "permission_mode": "default",
495 "hook_event_name": "PostCustomToolCall",1055 "hook_event_name": "PermissionRequest",
496 "tool_name": "mcp__github__create_issue",1056 "tool_name": "Bash",
497 "tool_input": {1057 "tool_input": {
498 "title": "Bug report",1058 "command": "rm -rf node_modules",
499 "body": "Description of the issue"1059 "description": "Remove node_modules directory"
1060 },
1061 "permission_suggestions": [
1062 {
1063 "type": "addRules",
1064 "rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
1065 "behavior": "allow",
1066 "destination": "localSettings"
1067 }
1068 ]
1069}
1070```
1071
1072#### PermissionRequest decision control
1073
1074`PermissionRequest` hooks can allow or deny permission requests. In addition to the [JSON output fields](#json-output) available to all hooks, your hook script can return a `decision` object with these event-specific fields:
1075
1076| Field | Description |
1077| :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1078| `behavior` | `"allow"` grants the permission, `"deny"` denies it |
1079| `updatedInput` | For `"allow"` only: modifies the tool's input parameters before execution. Replaces the entire input object, so include unchanged fields alongside modified ones |
1080| `updatedPermissions` | For `"allow"` only: array of [permission update entries](#permission-update-entries) to apply, such as adding an allow rule or changing the session permission mode |
1081| `message` | For `"deny"` only: tells Claude why the permission was denied |
1082| `interrupt` | For `"deny"` only: if `true`, stops Claude |
1083
1084```json theme={null}
1085{
1086 "hookSpecificOutput": {
1087 "hookEventName": "PermissionRequest",
1088 "decision": {
1089 "behavior": "allow",
1090 "updatedInput": {
1091 "command": "npm run lint"
1092 }
1093 }
1094 }
1095}
1096```
1097
1098#### Permission update entries
1099
1100The `updatedPermissions` output field and the [`permission_suggestions` input field](#permissionrequest-input) both use the same array of entry objects. Each entry has a `type` that determines its other fields, and a `destination` that controls where the change is written.
1101
1102| `type` | Fields | Effect |
1103| :------------------ | :--------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1104| `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"` |
1105| `replaceRules` | `rules`, `behavior`, `destination` | Replaces all rules of the given `behavior` at the `destination` with the provided `rules` |
1106| `removeRules` | `rules`, `behavior`, `destination` | Removes matching rules of the given `behavior` |
1107| `setMode` | `mode`, `destination` | Changes the permission mode. Valid modes are `default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, and `plan` |
1108| `addDirectories` | `directories`, `destination` | Adds working directories. `directories` is an array of path strings |
1109| `removeDirectories` | `directories`, `destination` | Removes working directories |
1110
1111The `destination` field on every entry determines whether the change stays in memory or persists to a settings file.
1112
1113| `destination` | Writes to |
1114| :---------------- | :---------------------------------------------- |
1115| `session` | in-memory only, discarded when the session ends |
1116| `localSettings` | `.claude/settings.local.json` |
1117| `projectSettings` | `.claude/settings.json` |
1118| `userSettings` | `~/.claude/settings.json` |
1119
1120A 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.
1121
1122### PostToolUse
1123
1124Runs immediately after a tool completes successfully.
1125
1126Matches on tool name, same values as PreToolUse.
1127
1128#### PostToolUse input
1129
1130`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.
1131
1132```json theme={null}
1133{
1134 "session_id": "abc123",
1135 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1136 "cwd": "/Users/...",
1137 "permission_mode": "default",
1138 "hook_event_name": "PostToolUse",
1139 "tool_name": "Write",
1140 "tool_input": {
1141 "file_path": "/path/to/file.txt",
1142 "content": "file content"
500 },1143 },
501 "tool_response": {1144 "tool_response": {
502 "issue_number": 42,1145 "filePath": "/path/to/file.txt",
503 "url": "https://github.com/org/repo/issues/42"1146 "success": true
1147 },
1148 "tool_use_id": "toolu_01ABC123..."
1149}
1150```
1151
1152#### PostToolUse decision control
1153
1154`PostToolUse` hooks can provide feedback to Claude after tool execution. In addition to the [JSON output fields](#json-output) available to all hooks, your hook script can return these event-specific fields:
1155
1156| Field | Description |
1157| :--------------------- | :----------------------------------------------------------------------------------------- |
1158| `decision` | `"block"` prompts Claude with the `reason`. Omit to allow the action to proceed |
1159| `reason` | Explanation shown to Claude when `decision` is `"block"` |
1160| `additionalContext` | Additional context for Claude to consider |
1161| `updatedMCPToolOutput` | For [MCP tools](#match-mcp-tools) only: replaces the tool's output with the provided value |
1162
1163```json theme={null}
1164{
1165 "decision": "block",
1166 "reason": "Explanation for decision",
1167 "hookSpecificOutput": {
1168 "hookEventName": "PostToolUse",
1169 "additionalContext": "Additional information for Claude"
1170 }
1171}
1172```
1173
1174### PostToolUseFailure
1175
1176Runs 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.
1177
1178Matches on tool name, same values as PreToolUse.
1179
1180#### PostToolUseFailure input
1181
1182PostToolUseFailure hooks receive the same `tool_name` and `tool_input` fields as PostToolUse, along with error information as top-level fields:
1183
1184```json theme={null}
1185{
1186 "session_id": "abc123",
1187 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1188 "cwd": "/Users/...",
1189 "permission_mode": "default",
1190 "hook_event_name": "PostToolUseFailure",
1191 "tool_name": "Bash",
1192 "tool_input": {
1193 "command": "npm test",
1194 "description": "Run test suite"
1195 },
1196 "tool_use_id": "toolu_01ABC123...",
1197 "error": "Command exited with non-zero status code 1",
1198 "is_interrupt": false
1199}
1200```
1201
1202| Field | Description |
1203| :------------- | :------------------------------------------------------------------------------ |
1204| `error` | String describing what went wrong |
1205| `is_interrupt` | Optional boolean indicating whether the failure was caused by user interruption |
1206
1207#### PostToolUseFailure decision control
1208
1209`PostToolUseFailure` hooks can provide context to Claude after a tool failure. In addition to the [JSON output fields](#json-output) available to all hooks, your hook script can return these event-specific fields:
1210
1211| Field | Description |
1212| :------------------ | :------------------------------------------------------------ |
1213| `additionalContext` | Additional context for Claude to consider alongside the error |
1214
1215```json theme={null}
1216{
1217 "hookSpecificOutput": {
1218 "hookEventName": "PostToolUseFailure",
1219 "additionalContext": "Additional information about the failure for Claude"
1220 }
1221}
1222```
1223
1224### PermissionDenied
1225
1226Runs when the [auto mode](/en/permission-modes#eliminate-prompts-with-auto-mode) classifier denies a tool call. This hook only fires in auto mode: it does not 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.
1227
1228Matches on tool name, same values as PreToolUse.
1229
1230#### PermissionDenied input
1231
1232In addition to the [common input fields](#common-input-fields), PermissionDenied hooks receive `tool_name`, `tool_input`, `tool_use_id`, and `reason`.
1233
1234```json theme={null}
1235{
1236 "session_id": "abc123",
1237 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1238 "cwd": "/Users/...",
1239 "permission_mode": "auto",
1240 "hook_event_name": "PermissionDenied",
1241 "tool_name": "Bash",
1242 "tool_input": {
1243 "command": "rm -rf /tmp/build",
1244 "description": "Clean build directory"
1245 },
1246 "tool_use_id": "toolu_01ABC123...",
1247 "reason": "Auto mode denied: command targets a path outside the project"
1248}
1249```
1250
1251| Field | Description |
1252| :------- | :------------------------------------------------------------ |
1253| `reason` | The classifier's explanation for why the tool call was denied |
1254
1255#### PermissionDenied decision control
1256
1257PermissionDenied hooks can tell the model it may retry the denied tool call. Return a JSON object with `hookSpecificOutput.retry` set to `true`:
1258
1259```json theme={null}
1260{
1261 "hookSpecificOutput": {
1262 "hookEventName": "PermissionDenied",
1263 "retry": true
1264 }
1265}
1266```
1267
1268When `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 does not return JSON, or returns `retry: false`, the denial stands and the model receives the original rejection message.
1269
1270### Notification
1271
1272Runs when Claude Code sends notifications. Matches on notification type: `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`. Omit the matcher to run hooks for all notification types.
1273
1274Use 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:
1275
1276```json theme={null}
1277{
1278 "hooks": {
1279 "Notification": [
1280 {
1281 "matcher": "permission_prompt",
1282 "hooks": [
1283 {
1284 "type": "command",
1285 "command": "/path/to/permission-alert.sh"
1286 }
1287 ]
1288 },
1289 {
1290 "matcher": "idle_prompt",
1291 "hooks": [
1292 {
1293 "type": "command",
1294 "command": "/path/to/idle-notification.sh"
1295 }
1296 ]
1297 }
1298 ]
1299 }
1300}
1301```
1302
1303#### Notification input
1304
1305In addition to the [common input fields](#common-input-fields), Notification hooks receive `message` with the notification text, an optional `title`, and `notification_type` indicating which type fired.
1306
1307```json theme={null}
1308{
1309 "session_id": "abc123",
1310 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1311 "cwd": "/Users/...",
1312 "hook_event_name": "Notification",
1313 "message": "Claude needs your permission to use Bash",
1314 "title": "Permission needed",
1315 "notification_type": "permission_prompt"
1316}
1317```
1318
1319Notification hooks cannot block or modify notifications. In addition to the [JSON output fields](#json-output) available to all hooks, you can return `additionalContext` to add context to the conversation:
1320
1321| Field | Description |
1322| :------------------ | :------------------------------- |
1323| `additionalContext` | String added to Claude's context |
1324
1325### SubagentStart
1326
1327Runs when a Claude Code subagent is spawned via the Agent tool. Supports matchers to filter by agent type name (built-in agents like `Bash`, `Explore`, `Plan`, or custom agent names from `.claude/agents/`).
1328
1329#### SubagentStart input
1330
1331In addition to the [common input fields](#common-input-fields), SubagentStart hooks receive `agent_id` with the unique identifier for the subagent and `agent_type` with the agent name (built-in agents like `"Bash"`, `"Explore"`, `"Plan"`, or custom agent names).
1332
1333```json theme={null}
1334{
1335 "session_id": "abc123",
1336 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1337 "cwd": "/Users/...",
1338 "hook_event_name": "SubagentStart",
1339 "agent_id": "agent-abc123",
1340 "agent_type": "Explore"
1341}
1342```
1343
1344SubagentStart hooks cannot block subagent creation, but they can inject context into the subagent. In addition to the [JSON output fields](#json-output) available to all hooks, you can return:
1345
1346| Field | Description |
1347| :------------------ | :------------------------------------- |
1348| `additionalContext` | String added to the subagent's context |
1349
1350```json theme={null}
1351{
1352 "hookSpecificOutput": {
1353 "hookEventName": "SubagentStart",
1354 "additionalContext": "Follow security guidelines for this task"
1355 }
1356}
1357```
1358
1359### SubagentStop
1360
1361Runs when a Claude Code subagent has finished responding. Matches on agent type, same values as SubagentStart.
1362
1363#### SubagentStop input
1364
1365In addition to the [common input fields](#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.
1366
1367```json theme={null}
1368{
1369 "session_id": "abc123",
1370 "transcript_path": "~/.claude/projects/.../abc123.jsonl",
1371 "cwd": "/Users/...",
1372 "permission_mode": "default",
1373 "hook_event_name": "SubagentStop",
1374 "stop_hook_active": false,
1375 "agent_id": "def456",
1376 "agent_type": "Explore",
1377 "agent_transcript_path": "~/.claude/projects/.../abc123/subagents/agent-def456.jsonl",
1378 "last_assistant_message": "Analysis complete. Found 3 potential issues..."
1379}
1380```
1381
1382SubagentStop hooks use the same decision control format as [Stop hooks](#stop-decision-control).
1383
1384### TaskCreated
1385
1386Runs 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.
1387
1388When 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 do not support matchers and fire on every occurrence.
1389
1390#### TaskCreated input
1391
1392In addition to the [common input fields](#common-input-fields), TaskCreated hooks receive `task_id`, `task_subject`, and optionally `task_description`, `teammate_name`, and `team_name`.
1393
1394```json theme={null}
1395{
1396 "session_id": "abc123",
1397 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1398 "cwd": "/Users/...",
1399 "permission_mode": "default",
1400 "hook_event_name": "TaskCreated",
1401 "task_id": "task-001",
1402 "task_subject": "Implement user authentication",
1403 "task_description": "Add login and signup endpoints",
1404 "teammate_name": "implementer",
1405 "team_name": "my-project"
1406}
1407```
1408
1409| Field | Description |
1410| :----------------- | :---------------------------------------------------- |
1411| `task_id` | Identifier of the task being created |
1412| `task_subject` | Title of the task |
1413| `task_description` | Detailed description of the task. May be absent |
1414| `teammate_name` | Name of the teammate creating the task. May be absent |
1415| `team_name` | Name of the team. May be absent |
1416
1417#### TaskCreated decision control
1418
1419TaskCreated hooks support two ways to control task creation:
1420
1421* **Exit code 2**: the task is not created and the stderr message is fed back to the model as feedback.
1422* **JSON `{"continue": false, "stopReason": "..."}`**: stops the teammate entirely, matching `Stop` hook behavior. The `stopReason` is shown to the user.
1423
1424This example blocks tasks whose subjects don't follow the required format:
1425
1426```bash theme={null}
1427#!/bin/bash
1428INPUT=$(cat)
1429TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')
1430
1431if [[ ! "$TASK_SUBJECT" =~ ^\[TICKET-[0-9]+\] ]]; then
1432 echo "Task subject must start with a ticket number, e.g. '[TICKET-123] Add feature'" >&2
1433 exit 2
1434fi
1435
1436exit 0
1437```
1438
1439### TaskCompleted
1440
1441Runs 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](/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.
1442
1443When 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 do not support matchers and fire on every occurrence.
1444
1445#### TaskCompleted input
1446
1447In addition to the [common input fields](#common-input-fields), TaskCompleted hooks receive `task_id`, `task_subject`, and optionally `task_description`, `teammate_name`, and `team_name`.
1448
1449```json theme={null}
1450{
1451 "session_id": "abc123",
1452 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1453 "cwd": "/Users/...",
1454 "permission_mode": "default",
1455 "hook_event_name": "TaskCompleted",
1456 "task_id": "task-001",
1457 "task_subject": "Implement user authentication",
1458 "task_description": "Add login and signup endpoints",
1459 "teammate_name": "implementer",
1460 "team_name": "my-project"
1461}
1462```
1463
1464| Field | Description |
1465| :----------------- | :------------------------------------------------------ |
1466| `task_id` | Identifier of the task being completed |
1467| `task_subject` | Title of the task |
1468| `task_description` | Detailed description of the task. May be absent |
1469| `teammate_name` | Name of the teammate completing the task. May be absent |
1470| `team_name` | Name of the team. May be absent |
1471
1472#### TaskCompleted decision control
1473
1474TaskCompleted hooks support two ways to control task completion:
1475
1476* **Exit code 2**: the task is not marked as completed and the stderr message is fed back to the model as feedback.
1477* **JSON `{"continue": false, "stopReason": "..."}`**: stops the teammate entirely, matching `Stop` hook behavior. The `stopReason` is shown to the user.
1478
1479This example runs tests and blocks task completion if they fail:
1480
1481```bash theme={null}
1482#!/bin/bash
1483INPUT=$(cat)
1484TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')
1485
1486# Run the test suite
1487if ! npm test 2>&1; then
1488 echo "Tests not passing. Fix failing tests before completing: $TASK_SUBJECT" >&2
1489 exit 2
1490fi
1491
1492exit 0
1493```
1494
1495### Stop
1496
1497Runs when the main Claude Code agent has finished responding. Does not run if
1498the stoppage occurred due to a user interrupt. API errors fire
1499[StopFailure](#stopfailure) instead.
1500
1501#### Stop input
1502
1503In addition to the [common input fields](#common-input-fields), Stop hooks receive `stop_hook_active` and `last_assistant_message`. 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 prevent Claude Code from running indefinitely. The `last_assistant_message` field contains the text content of Claude's final response, so hooks can access it without parsing the transcript file.
1504
1505```json theme={null}
1506{
1507 "session_id": "abc123",
1508 "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1509 "cwd": "/Users/...",
1510 "permission_mode": "default",
1511 "hook_event_name": "Stop",
1512 "stop_hook_active": true,
1513 "last_assistant_message": "I've completed the refactoring. Here's a summary..."
1514}
1515```
1516
1517#### Stop decision control
1518
1519`Stop` and `SubagentStop` hooks can control whether Claude continues. In addition to the [JSON output fields](#json-output) available to all hooks, your hook script can return these event-specific fields:
1520
1521| Field | Description |
1522| :--------- | :------------------------------------------------------------------------- |
1523| `decision` | `"block"` prevents Claude from stopping. Omit to allow Claude to stop |
1524| `reason` | Required when `decision` is `"block"`. Tells Claude why it should continue |
1525
1526```json theme={null}
1527{
1528 "decision": "block",
1529 "reason": "Must be provided when Claude is blocked from stopping"
1530}
1531```
1532
1533### StopFailure
1534
1535Runs instead of [Stop](#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 cannot complete a response due to rate limits, authentication problems, or other API errors.
1536
1537#### StopFailure input
1538
1539In addition to the [common input fields](#common-input-fields), StopFailure hooks receive `error`, optional `error_details`, and optional `last_assistant_message`. The `error` field identifies the error type and is used for matcher filtering.
1540
1541| Field | Description |
1542| :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1543| `error` | Error type: `rate_limit`, `authentication_failed`, `billing_error`, `invalid_request`, `server_error`, `max_output_tokens`, or `unknown` |
1544| `error_details` | Additional details about the error, when available |
1545| `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"` |
1546
1547```json theme={null}
1548{
1549 "session_id": "abc123",
1550 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1551 "cwd": "/Users/...",
1552 "hook_event_name": "StopFailure",
1553 "error": "rate_limit",
1554 "error_details": "429 Too Many Requests",
1555 "last_assistant_message": "API Error: Rate limit reached"
1556}
1557```
1558
1559StopFailure hooks have no decision control. They run for notification and logging purposes only.
1560
1561### TeammateIdle
1562
1563Runs when an [agent team](/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.
1564
1565When 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 do not support matchers and fire on every occurrence.
1566
1567#### TeammateIdle input
1568
1569In addition to the [common input fields](#common-input-fields), TeammateIdle hooks receive `teammate_name` and `team_name`.
1570
1571```json theme={null}
1572{
1573 "session_id": "abc123",
1574 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1575 "cwd": "/Users/...",
1576 "permission_mode": "default",
1577 "hook_event_name": "TeammateIdle",
1578 "teammate_name": "researcher",
1579 "team_name": "my-project"
1580}
1581```
1582
1583| Field | Description |
1584| :-------------- | :-------------------------------------------- |
1585| `teammate_name` | Name of the teammate that is about to go idle |
1586| `team_name` | Name of the team |
1587
1588#### TeammateIdle decision control
1589
1590TeammateIdle hooks support two ways to control teammate behavior:
1591
1592* **Exit code 2**: the teammate receives the stderr message as feedback and continues working instead of going idle.
1593* **JSON `{"continue": false, "stopReason": "..."}`**: stops the teammate entirely, matching `Stop` hook behavior. The `stopReason` is shown to the user.
1594
1595This example checks that a build artifact exists before allowing a teammate to go idle:
1596
1597```bash theme={null}
1598#!/bin/bash
1599
1600if [ ! -f "./dist/output.js" ]; then
1601 echo "Build artifact missing. Run the build before stopping." >&2
1602 exit 2
1603fi
1604
1605exit 0
1606```
1607
1608### ConfigChange
1609
1610Runs when a configuration file changes during a session. Use this to audit settings changes, enforce security policies, or block unauthorized modifications to configuration files.
1611
1612ConfigChange 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.
1613
1614The matcher filters on the configuration source:
1615
1616| Matcher | When it fires |
1617| :----------------- | :---------------------------------------- |
1618| `user_settings` | `~/.claude/settings.json` changes |
1619| `project_settings` | `.claude/settings.json` changes |
1620| `local_settings` | `.claude/settings.local.json` changes |
1621| `policy_settings` | Managed policy settings change |
1622| `skills` | A skill file in `.claude/skills/` changes |
1623
1624This example logs all configuration changes for security auditing:
1625
1626```json theme={null}
1627{
1628 "hooks": {
1629 "ConfigChange": [
1630 {
1631 "hooks": [
1632 {
1633 "type": "command",
1634 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-config-change.sh"
1635 }
1636 ]
1637 }
1638 ]
1639 }
1640}
1641```
1642
1643#### ConfigChange input
1644
1645In addition to the [common input fields](#common-input-fields), ConfigChange hooks receive `source` and optionally `file_path`. The `source` field indicates which configuration type changed, and `file_path` provides the path to the specific file that was modified.
1646
1647```json theme={null}
1648{
1649 "session_id": "abc123",
1650 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
1651 "cwd": "/Users/...",
1652 "hook_event_name": "ConfigChange",
1653 "source": "project_settings",
1654 "file_path": "/Users/.../my-project/.claude/settings.json"
1655}
1656```
1657
1658#### ConfigChange decision control
1659
1660ConfigChange hooks can block configuration changes from taking effect. Use exit code 2 or a JSON `decision` to prevent the change. When blocked, the new settings are not applied to the running session.
1661
1662| Field | Description |
1663| :--------- | :--------------------------------------------------------------------------------------- |
1664| `decision` | `"block"` prevents the configuration change from being applied. Omit to allow the change |
1665| `reason` | Explanation shown to the user when `decision` is `"block"` |
1666
1667```json theme={null}
1668{
1669 "decision": "block",
1670 "reason": "Configuration changes to project settings require admin approval"
1671}
1672```
1673
1674`policy_settings` changes cannot 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.
1675
1676### CwdChanged
1677
1678Runs 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](#filechanged) for tools like [direnv](https://direnv.net/) that manage per-directory environment.
1679
1680CwdChanged 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](#persist-environment-variables). Only `type: "command"` hooks are supported.
1681
1682CwdChanged does not support matchers and fires on every directory change.
1683
1684#### CwdChanged input
1685
1686In addition to the [common input fields](#common-input-fields), CwdChanged hooks receive `old_cwd` and `new_cwd`.
1687
1688```json theme={null}
1689{
1690 "session_id": "abc123",
1691 "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
1692 "cwd": "/Users/my-project/src",
1693 "hook_event_name": "CwdChanged",
1694 "old_cwd": "/Users/my-project",
1695 "new_cwd": "/Users/my-project/src"
1696}
1697```
1698
1699#### CwdChanged output
1700
1701In addition to the [JSON output fields](#json-output) available to all hooks, CwdChanged hooks can return `watchPaths` to dynamically set which file paths [FileChanged](#filechanged) watches:
1702
1703| Field | Description |
1704| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1705| `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 |
1706
1707CwdChanged hooks have no decision control. They cannot block the directory change.
1708
1709### FileChanged
1710
1711Runs when a watched file changes on disk. The `matcher` field in your hook configuration controls which filenames to watch: it is a pipe-separated list of basenames (filenames without directory paths, for example `".envrc|.env"`). The same `matcher` value is also used to filter which hooks run when a file changes, matching against the basename of the changed file. Useful for reloading environment variables when project configuration files are modified.
1712
1713FileChanged 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](#persist-environment-variables). Only `type: "command"` hooks are supported.
1714
1715#### FileChanged input
1716
1717In addition to the [common input fields](#common-input-fields), FileChanged hooks receive `file_path` and `event`.
1718
1719| Field | Description |
1720| :---------- | :---------------------------------------------------------------------------------------------- |
1721| `file_path` | Absolute path to the file that changed |
1722| `event` | What happened: `"change"` (file modified), `"add"` (file created), or `"unlink"` (file deleted) |
1723
1724```json theme={null}
1725{
1726 "session_id": "abc123",
1727 "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
1728 "cwd": "/Users/my-project",
1729 "hook_event_name": "FileChanged",
1730 "file_path": "/Users/my-project/.envrc",
1731 "event": "change"
1732}
1733```
1734
1735#### FileChanged output
1736
1737In addition to the [JSON output fields](#json-output) available to all hooks, FileChanged hooks can return `watchPaths` to dynamically update which file paths are watched:
1738
1739| Field | Description |
1740| :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1741| `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 |
1742
1743FileChanged hooks have no decision control. They cannot block the file change from occurring.
1744
1745### WorktreeCreate
1746
1747When you run `claude --worktree` or a [subagent uses `isolation: "worktree"`](/en/sub-agents#choose-the-subagent-scope), Claude Code creates an isolated working copy using `git worktree`. If you configure a WorktreeCreate hook, it replaces the default git behavior, letting you use a different version control system like SVN, Perforce, or Mercurial.
1748
1749Because the hook replaces the default behavior entirely, [`.worktreeinclude`](/en/common-workflows#copy-gitignored-files-to-worktrees) is not processed. If you need to copy local configuration files like `.env` into the new worktree, do it inside your hook script.
1750
1751The hook must return the absolute path to the created worktree directory. Claude Code uses this path as the working directory for the isolated session. Command hooks print it on stdout; HTTP hooks return it via `hookSpecificOutput.worktreePath`.
1752
1753This example creates an SVN working copy and prints the path for Claude Code to use. Replace the repository URL with your own:
1754
1755```json theme={null}
1756{
1757 "hooks": {
1758 "WorktreeCreate": [
1759 {
1760 "hooks": [
1761 {
1762 "type": "command",
1763 "command": "bash -c 'NAME=$(jq -r .name); DIR=\"$HOME/.claude/worktrees/$NAME\"; svn checkout https://svn.example.com/repo/trunk \"$DIR\" >&2 && echo \"$DIR\"'"
1764 }
1765 ]
1766 }
1767 ]
504 }1768 }
505}1769}
506```1770```
507 1771
508### Notification Input1772The hook reads the worktree `name` from the JSON input on stdin, checks out a fresh copy into a new directory, and prints the directory path. The `echo` on the last line is what Claude Code reads as the worktree path. Redirect any other output to stderr so it doesn't interfere with the path.
1773
1774#### WorktreeCreate input
1775
1776In addition to the [common input fields](#common-input-fields), WorktreeCreate hooks receive the `name` field. This is a slug identifier for the new worktree, either specified by the user or auto-generated (for example, `bold-oak-a3f2`).
509 1777
510```json theme={null}1778```json theme={null}
511{1779{
512 "session_id": "abc123",1780 "session_id": "abc123",
513 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",1781 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
514 "cwd": "/Users/...",1782 "cwd": "/Users/...",
515 "permission_mode": "default",1783 "hook_event_name": "WorktreeCreate",
516 "hook_event_name": "Notification",1784 "name": "feature-auth"
517 "message": "Task completed successfully"
518}1785}
519```1786```
520 1787
521### UserPromptSubmit Input1788#### WorktreeCreate output
1789
1790WorktreeCreate hooks do not use the standard allow/block decision model. Instead, the hook's success or failure determines the outcome. The hook must return the absolute path to the created worktree directory:
1791
1792* **Command hooks** (`type: "command"`): print the path on stdout.
1793* **HTTP hooks** (`type: "http"`): return `{ "hookSpecificOutput": { "hookEventName": "WorktreeCreate", "worktreePath": "/absolute/path" } }` in the response body.
1794
1795If the hook fails or produces no path, worktree creation fails with an error.
1796
1797### WorktreeRemove
1798
1799The cleanup counterpart to [WorktreeCreate](#worktreecreate). This hook fires 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. For git-based worktrees, Claude 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.
1800
1801Claude Code passes the path returned by WorktreeCreate as `worktree_path` in the hook input. This example reads that path and removes the directory:
522 1802
523```json theme={null}1803```json theme={null}
524{1804{
525 "session_id": "abc123",1805 "hooks": {
526 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",1806 "WorktreeRemove": [
527 "cwd": "/Users/...",1807 {
528 "permission_mode": "default",1808 "hooks": [
529 "hook_event_name": "UserPromptSubmit",1809 {
530 "prompt": "Write a function to calculate the factorial of a number"1810 "type": "command",
1811 "command": "bash -c 'jq -r .worktree_path | xargs rm -rf'"
1812 }
1813 ]
1814 }
1815 ]
1816 }
531}1817}
532```1818```
533 1819
534### Stop and SubagentStop Input1820#### WorktreeRemove input
535 1821
536`stop_hook_active` is true when Claude Code is already continuing as a result of1822In addition to the [common input fields](#common-input-fields), WorktreeRemove hooks receive the `worktree_path` field, which is the absolute path to the worktree being removed.
537a stop hook. Check this value or process the transcript to prevent Claude Code
538from running indefinitely.
539 1823
540```json theme={null}1824```json theme={null}
541{1825{
542 "session_id": "abc123",1826 "session_id": "abc123",
543 "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",1827 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
544 "permission_mode": "default",1828 "cwd": "/Users/...",
545 "hook_event_name": "Stop",1829 "hook_event_name": "WorktreeRemove",
546 "stop_hook_active": true1830 "worktree_path": "/Users/.../my-project/.claude/worktrees/feature-auth"
547}1831}
548```1832```
549 1833
550### PreCompact Input1834WorktreeRemove hooks have no decision control. They cannot block worktree removal but can perform cleanup tasks like removing version control state or archiving changes. Hook failures are logged in debug mode only.
1835
1836### PreCompact
1837
1838Runs before Claude Code is about to run a compact operation.
1839
1840The matcher value indicates whether compaction was triggered manually or automatically:
1841
1842| Matcher | When it fires |
1843| :------- | :------------------------------------------- |
1844| `manual` | `/compact` |
1845| `auto` | Auto-compact when the context window is full |
551 1846
552For `manual`, `custom_instructions` comes from what the user passes into1847#### PreCompact input
553`/compact`. For `auto`, `custom_instructions` is empty.1848
1849In addition to the [common input fields](#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.
554 1850
555```json theme={null}1851```json theme={null}
556{1852{
557 "session_id": "abc123",1853 "session_id": "abc123",
558 "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",1854 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
559 "permission_mode": "default",1855 "cwd": "/Users/...",
560 "hook_event_name": "PreCompact",1856 "hook_event_name": "PreCompact",
561 "trigger": "manual",1857 "trigger": "manual",
562 "custom_instructions": ""1858 "custom_instructions": ""
563}1859}
564```1860```
565 1861
566### SessionStart Input1862### PostCompact
567 1863
568```json theme={null}1864Runs 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.
569{1865
570 "session_id": "abc123",1866The same matcher values apply as for `PreCompact`:
571 "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
572 "permission_mode": "default",
573 "hook_event_name": "SessionStart",
574 "source": "startup"
575}
576```
577 1867
578### SessionEnd Input1868| Matcher | When it fires |
1869| :------- | :------------------------------------------------- |
1870| `manual` | After `/compact` |
1871| `auto` | After auto-compact when the context window is full |
1872
1873#### PostCompact input
1874
1875In addition to the [common input fields](#common-input-fields), PostCompact hooks receive `trigger` and `compact_summary`. The `compact_summary` field contains the conversation summary generated by the compact operation.
579 1876
580```json theme={null}1877```json theme={null}
581{1878{
582 "session_id": "abc123",1879 "session_id": "abc123",
583 "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",1880 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
584 "cwd": "/Users/...",1881 "cwd": "/Users/...",
585 "permission_mode": "default",1882 "hook_event_name": "PostCompact",
586 "hook_event_name": "SessionEnd",1883 "trigger": "manual",
587 "reason": "exit"1884 "compact_summary": "Summary of the compacted conversation..."
588}1885}
589```1886```
590 1887
591## Hook Output1888PostCompact hooks have no decision control. They cannot affect the compaction result but can perform follow-up tasks.
592
593There are two ways for hooks to return output back to Claude Code. The output
594communicates whether to block and any feedback that should be shown to Claude
595and the user.
596 1889
597### Simple: Exit Code1890### SessionEnd
598
599Hooks communicate status through exit codes, stdout, and stderr:
600
601* **Exit code 0**: Success. `stdout` is shown to the user in transcript mode
602 (CTRL-R), except for `UserPromptSubmit` and `SessionStart`, where stdout is
603 added to the context.
604* **Exit code 2**: Blocking error. `stderr` is fed back to Claude to process
605 automatically. See per-hook-event behavior below.
606* **Other exit codes**: Non-blocking error. `stderr` is shown to the user and
607 execution continues.
608
609<Warning>
610 Reminder: Claude Code does not see stdout if the exit code is 0, except for
611 the `UserPromptSubmit` hook where stdout is injected as context.
612</Warning>
613
614#### Exit Code 2 Behavior
615 1891
616| Hook Event | Behavior |1892Runs when a Claude Code session ends. Useful for cleanup tasks, logging session
617| ------------------ | ------------------------------------------------------------------ |1893statistics, or saving session state. Supports matchers to filter by exit reason.
618| `PreToolUse` | Blocks the tool call, shows stderr to Claude |
619| `PostToolUse` | Shows stderr to Claude (tool already ran) |
620| `Notification` | N/A, shows stderr to user only |
621| `UserPromptSubmit` | Blocks prompt processing, erases prompt, shows stderr to user only |
622| `Stop` | Blocks stoppage, shows stderr to Claude |
623| `SubagentStop` | Blocks stoppage, shows stderr to Claude subagent |
624| `PreCompact` | N/A, shows stderr to user only |
625| `SessionStart` | N/A, shows stderr to user only |
626| `SessionEnd` | N/A, shows stderr to user only |
627 1894
628### Advanced: JSON Output1895The `reason` field in the hook input indicates why the session ended:
629 1896
630Hooks can return structured JSON in `stdout` for more sophisticated control:1897| Reason | Description |
1898| :---------------------------- | :----------------------------------------- |
1899| `clear` | Session cleared with `/clear` command |
1900| `resume` | Session switched via interactive `/resume` |
1901| `logout` | User logged out |
1902| `prompt_input_exit` | User exited while prompt input was visible |
1903| `bypass_permissions_disabled` | Bypass permissions mode was disabled |
1904| `other` | Other exit reasons |
631 1905
632#### Common JSON Fields1906#### SessionEnd input
633 1907
634All hook types can include these optional fields:1908In addition to the [common input fields](#common-input-fields), SessionEnd hooks receive a `reason` field indicating why the session ended. See the [reason table](#sessionend) above for all values.
635 1909
636```json theme={null}1910```json theme={null}
637{1911{
638 "continue": true, // Whether Claude should continue after hook execution (default: true)1912 "session_id": "abc123",
639 "stopReason": "string", // Message shown when continue is false1913 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
640 1914 "cwd": "/Users/...",
641 "suppressOutput": true, // Hide stdout from transcript mode (default: false)1915 "hook_event_name": "SessionEnd",
642 "systemMessage": "string" // Optional warning message shown to the user1916 "reason": "other"
643}1917}
644```1918```
645 1919
646If `continue` is false, Claude stops processing after the hooks run.1920SessionEnd hooks have no decision control. They cannot block session termination but can perform cleanup tasks.
1921
1922SessionEnd hooks have a default timeout of 1.5 seconds. This applies to session exit, `/clear`, and switching sessions via interactive `/resume`. If your hooks need more time, set the `CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS` environment variable to a higher value in milliseconds. Any per-hook `timeout` setting is also capped by this value.
647 1923
648* For `PreToolUse`, this is different from `"permissionDecision": "deny"`, which1924```bash theme={null}
649 only blocks a specific tool call and provides automatic feedback to Claude.1925CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=5000 claude
650* For `PostToolUse`, this is different from `"decision": "block"`, which1926```
651 provides automated feedback to Claude.
652* For `UserPromptSubmit`, this prevents the prompt from being processed.
653* For `Stop` and `SubagentStop`, this takes precedence over any
654 `"decision": "block"` output.
655* In all cases, `"continue" = false` takes precedence over any
656 `"decision": "block"` output.
657 1927
658`stopReason` accompanies `continue` with a reason shown to the user, not shown1928### Elicitation
659to Claude.
660 1929
661#### `PreToolUse` Decision Control1930Runs 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.
662 1931
663`PreToolUse` hooks can control whether a tool call proceeds.1932The matcher field matches against the MCP server name.
664 1933
665* `"allow"` bypasses the permission system. `permissionDecisionReason` is shown1934#### Elicitation input
666 to the user but not to Claude.
667* `"deny"` prevents the tool call from executing. `permissionDecisionReason` is
668 shown to Claude.
669* `"ask"` asks the user to confirm the tool call in the UI.
670 `permissionDecisionReason` is shown to the user but not to Claude.
671 1935
672Additionally, hooks can modify tool inputs before execution using `updatedInput`:1936In addition to the [common input fields](#common-input-fields), Elicitation hooks receive `mcp_server_name`, `message`, and optional `mode`, `url`, `elicitation_id`, and `requested_schema` fields.
673 1937
674* `updatedInput` allows you to modify the tool's input parameters before the tool executes. This is a `Record<string, unknown>` object containing the fields you want to change or add.1938For form-mode elicitation (the most common case):
675* This is most useful with `"permissionDecision": "allow"` to modify and approve tool calls.
676 1939
677```json theme={null}1940```json theme={null}
678{1941{
679 "hookSpecificOutput": {1942 "session_id": "abc123",
680 "hookEventName": "PreToolUse",1943 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
681 "permissionDecision": "allow"1944 "cwd": "/Users/...",
682 "permissionDecisionReason": "My reason here",1945 "permission_mode": "default",
683 "updatedInput": {1946 "hook_event_name": "Elicitation",
684 "field_to_modify": "new value"1947 "mcp_server_name": "my-mcp-server",
1948 "message": "Please provide your credentials",
1949 "mode": "form",
1950 "requested_schema": {
1951 "type": "object",
1952 "properties": {
1953 "username": { "type": "string", "title": "Username" }
685 }1954 }
686 }1955 }
687}1956}
688```1957```
689 1958
690<Note>1959For URL-mode elicitation (browser-based authentication):
691 The `decision` and `reason` fields are deprecated for PreToolUse hooks.
692 Use `hookSpecificOutput.permissionDecision` and
693 `hookSpecificOutput.permissionDecisionReason` instead. The deprecated fields
694 `"approve"` and `"block"` map to `"allow"` and `"deny"` respectively.
695</Note>
696
697#### `PostToolUse` Decision Control
698
699`PostToolUse` hooks can provide feedback to Claude after tool execution.
700
701* `"block"` automatically prompts Claude with `reason`.
702* `undefined` does nothing. `reason` is ignored.
703* `"hookSpecificOutput.additionalContext"` adds context for Claude to consider.
704 1960
705```json theme={null}1961```json theme={null}
706{1962{
707 "decision": "block" | undefined,1963 "session_id": "abc123",
708 "reason": "Explanation for decision",1964 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
709 "hookSpecificOutput": {1965 "cwd": "/Users/...",
710 "hookEventName": "PostToolUse",1966 "permission_mode": "default",
711 "additionalContext": "Additional information for Claude"1967 "hook_event_name": "Elicitation",
712 }1968 "mcp_server_name": "my-mcp-server",
1969 "message": "Please authenticate",
1970 "mode": "url",
1971 "url": "https://auth.example.com/login"
713}1972}
714```1973```
715 1974
716#### `PostCustomToolCall` Output Control1975#### Elicitation output
717
718`PostCustomToolCall` hooks can modify MCP tool outputs before they're processed further.
719 1976
720* `"hookSpecificOutput.updatedOutput"` replaces the original tool response1977To respond programmatically without showing the dialog, return a JSON object with `hookSpecificOutput`:
721* The modified output is shown to Claude instead of the original response
722* This runs **before** `PostToolUse` hooks, so they see the modified output
723 1978
724```json theme={null}1979```json theme={null}
725{1980{
726 "hookSpecificOutput": {1981 "hookSpecificOutput": {
727 "hookEventName": "PostCustomToolCall",1982 "hookEventName": "Elicitation",
728 "updatedOutput": {1983 "action": "accept",
729 "issue_number": 42,1984 "content": {
730 "url": "https://github.com/org/repo/issues/42",1985 "username": "alice"
731 "hook_processed": true,
732 "processed_at": "2025-01-01T00:00:00Z"
733 }1986 }
734 }1987 }
735}1988}
736```1989```
737 1990
738**Example: Adding metadata to MCP tool responses**1991| Field | Values | Description |
1992| :-------- | :---------------------------- | :--------------------------------------------------------------- |
1993| `action` | `accept`, `decline`, `cancel` | Whether to accept, decline, or cancel the request |
1994| `content` | object | Form field values to submit. Only used when `action` is `accept` |
739 1995
740```bash theme={null}1996Exit code 2 denies the elicitation and shows stderr to the user.
741#!/bin/bash
742# Read JSON input from stdin
743INPUT=$(cat)
744 1997
745# Extract tool information1998### ElicitationResult
746TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
747TOOL_RESPONSE=$(echo "$INPUT" | jq -r '.tool_response')
748 1999
749# Only process MCP tools2000Runs 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.
750if [[ "$TOOL_NAME" != mcp__* ]]; then
751 exit 0
752fi
753 2001
754# Add metadata to the response2002The matcher field matches against the MCP server name.
755UPDATED_OUTPUT=$(echo "$TOOL_RESPONSE" | jq '. + {"hook_processed": true, "processed_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}')
756 2003
757# Output the modified response2004#### ElicitationResult input
758jq -n --argjson output "$UPDATED_OUTPUT" '{
759 "hookSpecificOutput": {
760 "hookEventName": "PostCustomToolCall",
761 "updatedOutput": $output
762 }
763}'
764```
765 2005
766<Warning>2006In addition to the [common input fields](#common-input-fields), ElicitationResult hooks receive `mcp_server_name`, `action`, and optional `mode`, `elicitation_id`, and `content` fields.
767 If multiple hooks provide `updatedOutput` for the same tool call, conflicts may occur since hooks run in parallel. Only configure one hook per tool to modify output.
768</Warning>
769 2007
770#### `UserPromptSubmit` Decision Control2008```json theme={null}
2009{
2010 "session_id": "abc123",
2011 "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
2012 "cwd": "/Users/...",
2013 "permission_mode": "default",
2014 "hook_event_name": "ElicitationResult",
2015 "mcp_server_name": "my-mcp-server",
2016 "action": "accept",
2017 "content": { "username": "alice" },
2018 "mode": "form",
2019 "elicitation_id": "elicit-123"
2020}
2021```
771 2022
772`UserPromptSubmit` hooks can control whether a user prompt is processed.2023#### ElicitationResult output
773 2024
774* `"block"` prevents the prompt from being processed. The submitted prompt is2025To override the user's response, return a JSON object with `hookSpecificOutput`:
775 erased from context. `"reason"` is shown to the user but not added to context.
776* `undefined` allows the prompt to proceed normally. `"reason"` is ignored.
777* `"hookSpecificOutput.additionalContext"` adds the string to the context if not
778 blocked.
779 2026
780```json theme={null}2027```json theme={null}
781{2028{
782 "decision": "block" | undefined,
783 "reason": "Explanation for decision",
784 "hookSpecificOutput": {2029 "hookSpecificOutput": {
785 "hookEventName": "UserPromptSubmit",2030 "hookEventName": "ElicitationResult",
786 "additionalContext": "My additional context here"2031 "action": "decline",
2032 "content": {}
787 }2033 }
788}2034}
789```2035```
790 2036
791#### `Stop`/`SubagentStop` Decision Control2037| Field | Values | Description |
2038| :-------- | :---------------------------- | :--------------------------------------------------------------------- |
2039| `action` | `accept`, `decline`, `cancel` | Overrides the user's action |
2040| `content` | object | Overrides form field values. Only meaningful when `action` is `accept` |
2041
2042Exit code 2 blocks the response, changing the effective action to `decline`.
2043
2044## Prompt-based hooks
2045
2046In addition to command and HTTP 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.
2047
2048Events that support all four hook types (`command`, `http`, `prompt`, and `agent`):
2049
2050* `PermissionRequest`
2051* `PostToolUse`
2052* `PostToolUseFailure`
2053* `PreToolUse`
2054* `Stop`
2055* `SubagentStop`
2056* `TaskCompleted`
2057* `TaskCreated`
2058* `UserPromptSubmit`
2059
2060Events that support `command` and `http` hooks but not `prompt` or `agent`:
2061
2062* `ConfigChange`
2063* `CwdChanged`
2064* `Elicitation`
2065* `ElicitationResult`
2066* `FileChanged`
2067* `InstructionsLoaded`
2068* `Notification`
2069* `PermissionDenied`
2070* `PostCompact`
2071* `PreCompact`
2072* `SessionEnd`
2073* `StopFailure`
2074* `SubagentStart`
2075* `TeammateIdle`
2076* `WorktreeCreate`
2077* `WorktreeRemove`
2078
2079`SessionStart` supports only `command` hooks.
792 2080
793`Stop` and `SubagentStop` hooks can control whether Claude must continue.2081### How prompt-based hooks work
794 2082
795* `"block"` prevents Claude from stopping. You must populate `reason` for Claude2083Instead of executing a Bash command, prompt-based hooks:
796 to know how to proceed.
797* `undefined` allows Claude to stop. `reason` is ignored.
798 2084
799```json theme={null}20851. Send the hook input and your prompt to a Claude model, Haiku by default
800{20862. The LLM responds with structured JSON containing a decision
801 "decision": "block" | undefined,20873. Claude Code processes the decision automatically
802 "reason": "Must be provided when Claude is blocked from stopping"
803}
804```
805 2088
806#### `SessionStart` Decision Control2089### Prompt hook configuration
807 2090
808`SessionStart` hooks allow you to load in context at the start of a session.2091Set `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.
809 2092
810* `"hookSpecificOutput.additionalContext"` adds the string to the context.2093This `Stop` hook asks the LLM to evaluate whether all tasks are complete before allowing Claude to finish:
811* Multiple hooks' `additionalContext` values are concatenated.
812 2094
813```json theme={null}2095```json theme={null}
814{2096{
815 "hookSpecificOutput": {2097 "hooks": {
816 "hookEventName": "SessionStart",2098 "Stop": [
817 "additionalContext": "My additional context here"2099 {
2100 "hooks": [
2101 {
2102 "type": "prompt",
2103 "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete."
2104 }
2105 ]
2106 }
2107 ]
818 }2108 }
819}2109}
820```2110```
821 2111
822#### `SessionEnd` Decision Control2112| Field | Required | Description |
823 2113| :-------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
824`SessionEnd` hooks run when a session ends. They cannot block session termination2114| `type` | yes | Must be `"prompt"` |
825but can perform cleanup tasks.2115| `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 |
826 2116| `model` | no | Model to use for evaluation. Defaults to a fast model |
827#### Exit Code Example: Bash Command Validation2117| `timeout` | no | Timeout in seconds. Default: 30 |
828
829```python theme={null}
830#!/usr/bin/env python3
831import json
832import re
833import sys
834
835# Define validation rules as a list of (regex pattern, message) tuples
836VALIDATION_RULES = [
837 (
838 r"\bgrep\b(?!.*\|)",
839 "Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
840 ),
841 (
842 r"\bfind\s+\S+\s+-name\b",
843 "Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance",
844 ),
845]
846
847 2118
848def validate_command(command: str) -> list[str]:2119### Response schema
849 issues = []
850 for pattern, message in VALIDATION_RULES:
851 if re.search(pattern, command):
852 issues.append(message)
853 return issues
854 2120
2121The LLM must respond with JSON containing:
855 2122
856try:2123```json theme={null}
857 input_data = json.load(sys.stdin)2124{
858except json.JSONDecodeError as e:2125 "ok": true | false,
859 print(f"Error: Invalid JSON input: {e}", file=sys.stderr)2126 "reason": "Explanation for the decision"
860 sys.exit(1)2127}
2128```
861 2129
862tool_name = input_data.get("tool_name", "")2130| Field | Description |
863tool_input = input_data.get("tool_input", {})2131| :------- | :--------------------------------------------------------- |
864command = tool_input.get("command", "")2132| `ok` | `true` allows the action, `false` prevents it |
2133| `reason` | Required when `ok` is `false`. Explanation shown to Claude |
865 2134
866if tool_name != "Bash" or not command:2135### Example: Multi-criteria Stop hook
867 sys.exit(1)
868 2136
869# Validate the command2137This `Stop` hook uses a detailed prompt to check three conditions before allowing Claude to stop. If `"ok"` is `false`, Claude continues working with the provided reason as its next instruction. `SubagentStop` hooks use the same format to evaluate whether a [subagent](/en/sub-agents) should stop:
870issues = validate_command(command)
871 2138
872if issues:2139```json theme={null}
873 for message in issues:2140{
874 print(f"• {message}", file=sys.stderr)2141 "hooks": {
875 # Exit code 2 blocks tool call and shows stderr to Claude2142 "Stop": [
876 sys.exit(2)2143 {
2144 "hooks": [
2145 {
2146 "type": "prompt",
2147 "prompt": "You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.",
2148 "timeout": 30
2149 }
2150 ]
2151 }
2152 ]
2153 }
2154}
877```2155```
878 2156
879#### JSON Output Example: UserPromptSubmit to Add Context and Validation2157## Agent-based hooks
880 2158
881<Note>2159Agent-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.
882 For `UserPromptSubmit` hooks, you can inject context using either method:
883 2160
884 * Exit code 0 with stdout: Claude sees the context (special case for `UserPromptSubmit`)2161### How agent hooks work
885 * JSON output: Provides more control over the behavior
886</Note>
887 2162
888```python theme={null}2163When an agent hook fires:
889#!/usr/bin/env python3
890import json
891import sys
892import re
893import datetime
894
895# Load input from stdin
896try:
897 input_data = json.load(sys.stdin)
898except json.JSONDecodeError as e:
899 print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
900 sys.exit(1)
901
902prompt = input_data.get("prompt", "")
903
904# Check for sensitive patterns
905sensitive_patterns = [
906 (r"(?i)\b(password|secret|key|token)\s*[:=]", "Prompt contains potential secrets"),
907]
908
909for pattern, message in sensitive_patterns:
910 if re.search(pattern, prompt):
911 # Use JSON output to block with a specific reason
912 output = {
913 "decision": "block",
914 "reason": f"Security policy violation: {message}. Please rephrase your request without sensitive information."
915 }
916 print(json.dumps(output))
917 sys.exit(0)
918 2164
919# Add current time to context21651. Claude Code spawns a subagent with your prompt and the hook's JSON input
920context = f"Current time: {datetime.datetime.now()}"21662. The subagent can use tools like Read, Grep, and Glob to investigate
921print(context)21673. After up to 50 turns, the subagent returns a structured `{ "ok": true/false }` decision
21684. Claude Code processes the decision the same way as a prompt hook
922 2169
923"""2170Agent hooks are useful when verification requires inspecting actual files or test output, not just evaluating the hook input data alone.
924The following is also equivalent:
925print(json.dumps({
926 "hookSpecificOutput": {
927 "hookEventName": "UserPromptSubmit",
928 "additionalContext": context,
929 },
930}))
931"""
932 2171
933# Allow the prompt to proceed with the additional context2172### Agent hook configuration
934sys.exit(0)
935```
936 2173
937#### JSON Output Example: PreToolUse with Approval2174Set `type` to `"agent"` and provide a `prompt` string. The configuration fields are the same as [prompt hooks](#prompt-hook-configuration), with a longer default timeout:
938 2175
939```python theme={null}2176| Field | Required | Description |
940#!/usr/bin/env python32177| :-------- | :------- | :------------------------------------------------------------------------------------------ |
941import json2178| `type` | yes | Must be `"agent"` |
942import sys2179| `prompt` | yes | Prompt describing what to verify. Use `$ARGUMENTS` as a placeholder for the hook input JSON |
2180| `model` | no | Model to use. Defaults to a fast model |
2181| `timeout` | no | Timeout in seconds. Default: 60 |
943 2182
944# Load input from stdin2183The response schema is the same as prompt hooks: `{ "ok": true }` to allow or `{ "ok": false, "reason": "..." }` to block.
945try:
946 input_data = json.load(sys.stdin)
947except json.JSONDecodeError as e:
948 print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
949 sys.exit(1)
950 2184
951tool_name = input_data.get("tool_name", "")2185This `Stop` hook verifies that all unit tests pass before allowing Claude to finish:
952tool_input = input_data.get("tool_input", {})
953 2186
954# Example: Auto-approve file reads for documentation files2187```json theme={null}
955if tool_name == "Read":2188{
956 file_path = tool_input.get("file_path", "")2189 "hooks": {
957 if file_path.endswith((".md", ".mdx", ".txt", ".json")):2190 "Stop": [
958 # Use JSON output to auto-approve the tool call2191 {
959 output = {2192 "hooks": [
960 "decision": "approve",2193 {
961 "reason": "Documentation file auto-approved",2194 "type": "agent",
962 "suppressOutput": True # Don't show in transcript mode2195 "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
2196 "timeout": 120
963 }2197 }
964 print(json.dumps(output))2198 ]
965 sys.exit(0)2199 }
966 2200 ]
967# For other cases, let the normal permission flow proceed2201 }
968sys.exit(0)2202}
969```2203```
970 2204
971## Working with MCP Tools2205## Run hooks in the background
972
973Claude Code hooks work seamlessly with
974[Model Context Protocol (MCP) tools](/en/docs/claude-code/mcp). When MCP servers
975provide tools, they appear with a special naming pattern that you can match in
976your hooks.
977
978### MCP Tool Naming
979 2206
980MCP tools follow the pattern `mcp__<server>__<tool>`, for example:2207By default, hooks block Claude's execution until they complete. For long-running tasks like deployments, test suites, or external API calls, set `"async": true` to run the hook in the background while Claude continues working. Async hooks cannot block or control Claude's behavior: response fields like `decision`, `permissionDecision`, and `continue` have no effect, because the action they would have controlled has already completed.
981 2208
982* `mcp__memory__create_entities` - Memory server's create entities tool2209### Configure an async hook
983* `mcp__filesystem__read_file` - Filesystem server's read file tool
984* `mcp__github__search_repositories` - GitHub server's search tool
985 2210
986### Configuring Hooks for MCP Tools2211Add `"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.
987 2212
988You can target specific MCP tools or entire MCP servers:2213This 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:
989 2214
990```json theme={null}2215```json theme={null}
991{2216{
992 "hooks": {2217 "hooks": {
993 "PreToolUse": [2218 "PostToolUse": [
994 {
995 "matcher": "mcp__memory__.*",
996 "hooks": [
997 {
998 "type": "command",
999 "command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
1000 }
1001 ]
1002 },
1003 {2219 {
1004 "matcher": "mcp__.*__write.*",2220 "matcher": "Write",
1005 "hooks": [2221 "hooks": [
1006 {2222 {
1007 "type": "command",2223 "type": "command",
1008 "command": "/home/user/scripts/validate-mcp-write.py"2224 "command": "/path/to/run-tests.sh",
2225 "async": true,
2226 "timeout": 120
1009 }2227 }
1010 ]2228 ]
1011 }2229 }
1014}2232}
1015```2233```
1016 2234
1017## Examples2235The `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.
1018
1019<Tip>
1020 For practical examples including code formatting, notifications, and file protection, see [More Examples](/en/docs/claude-code/hooks-guide#more-examples) in the get started guide.
1021</Tip>
1022 2236
1023## Security Considerations2237### How async hooks execute
1024 2238
1025### Disclaimer2239When 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.
1026 2240
1027**USE AT YOUR OWN RISK**: Claude Code hooks execute arbitrary shell commands on2241After the background process exits, if the hook produced a JSON response with a `systemMessage` or `additionalContext` field, that content is delivered to Claude as context on the next conversation turn.
1028your system automatically. By using hooks, you acknowledge that:
1029 2242
1030* You are solely responsible for the commands you configure2243Async hook completion notifications are suppressed by default. To see them, enable verbose mode with `Ctrl+O` or start Claude Code with `--verbose`.
1031* Hooks can modify, delete, or access any files your user account can access
1032* Malicious or poorly written hooks can cause data loss or system damage
1033* Anthropic provides no warranty and assumes no liability for any damages
1034 resulting from hook usage
1035* You should thoroughly test hooks in a safe environment before production use
1036 2244
1037Always review and understand any hook commands before adding them to your2245### Example: run tests after file changes
1038configuration.
1039 2246
1040### Security Best Practices2247This hook starts a test suite in the background whenever Claude writes a file, then reports the results back to Claude when the tests finish. Save this script to `.claude/hooks/run-tests-async.sh` in your project and make it executable with `chmod +x`:
1041 2248
1042Here are some key practices for writing more secure hooks:2249```bash theme={null}
2250#!/bin/bash
2251# run-tests-async.sh
1043 2252
10441. **Validate and sanitize inputs** - Never trust input data blindly2253# Read hook input from stdin
10452. **Always quote shell variables** - Use `"$VAR"` not `$VAR`2254INPUT=$(cat)
10463. **Block path traversal** - Check for `..` in file paths2255FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
10474. **Use absolute paths** - Specify full paths for scripts (use
1048 "\$CLAUDE\_PROJECT\_DIR" for the project path)
10495. **Skip sensitive files** - Avoid `.env`, `.git/`, keys, etc.
1050 2256
1051### Configuration Safety2257# Only run tests for source files
2258if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then
2259 exit 0
2260fi
1052 2261
1053Direct edits to hooks in settings files don't take effect immediately. Claude2262# Run tests and report results via systemMessage
1054Code:2263RESULT=$(npm test 2>&1)
2264EXIT_CODE=$?
1055 2265
10561. Captures a snapshot of hooks at startup2266if [ $EXIT_CODE -eq 0 ]; then
10572. Uses this snapshot throughout the session2267 echo "{\"systemMessage\": \"Tests passed after editing $FILE_PATH\"}"
10583. Warns if hooks are modified externally2268else
10594. Requires review in `/hooks` menu for changes to apply2269 echo "{\"systemMessage\": \"Tests failed after editing $FILE_PATH: $RESULT\"}"
2270fi
2271```
1060 2272
1061This prevents malicious hook modifications from affecting your current session.2273Then add this configuration to `.claude/settings.json` in your project root. The `async: true` flag lets Claude keep working while tests run:
1062 2274
1063## Hook Execution Details2275```json theme={null}
2276{
2277 "hooks": {
2278 "PostToolUse": [
2279 {
2280 "matcher": "Write|Edit",
2281 "hooks": [
2282 {
2283 "type": "command",
2284 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
2285 "async": true,
2286 "timeout": 300
2287 }
2288 ]
2289 }
2290 ]
2291 }
2292}
2293```
1064 2294
1065* **Timeout**: 60-second execution limit by default, configurable per command.2295### Limitations
1066 * A timeout for an individual command does not affect the other commands.
1067* **Parallelization**: All matching hooks run in parallel
1068* **Deduplication**: Multiple identical hook commands are deduplicated automatically
1069* **Environment**: Runs in current directory with Claude Code's environment
1070 * The `CLAUDE_PROJECT_DIR` environment variable is available and contains the
1071 absolute path to the project root directory (where Claude Code was started)
1072 * The `CLAUDE_CODE_REMOTE` environment variable indicates whether the hook is running in a remote (web) environment (`"true"`) or local CLI environment (not set or empty). Use this to run different logic based on execution context.
1073* **Input**: JSON via stdin
1074* **Output**:
1075 * PreToolUse/PostToolUse/Stop/SubagentStop: Progress shown in transcript (Ctrl-R)
1076 * Notification/SessionEnd: Logged to debug only (`--debug`)
1077 * UserPromptSubmit/SessionStart: stdout added as context for Claude
1078 2296
1079## Debugging2297Async hooks have several constraints compared to synchronous hooks:
1080 2298
1081### Basic Troubleshooting2299* Only `type: "command"` hooks support `async`. Prompt-based hooks cannot run asynchronously.
2300* Async hooks cannot block tool calls or return decisions. By the time the hook completes, the triggering action has already proceeded.
2301* Hook output is delivered on the next conversation turn. If the session is idle, the response waits until the next user interaction.
2302* Each execution creates a separate background process. There is no deduplication across multiple firings of the same async hook.
1082 2303
1083If your hooks aren't working:2304## Security considerations
1084 2305
10851. **Check configuration** - Run `/hooks` to see if your hook is registered2306### Disclaimer
10862. **Verify syntax** - Ensure your JSON settings are valid
10873. **Test commands** - Run hook commands manually first
10884. **Check permissions** - Make sure scripts are executable
10895. **Review logs** - Use `claude --debug` to see hook execution details
1090 2307
1091Common issues:2308Command hooks run with your system user's full permissions.
1092 2309
1093* **Quotes not escaped** - Use `\"` inside JSON strings2310<Warning>
1094* **Wrong matcher** - Check tool names match exactly (case-sensitive)2311 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.
1095* **Command not found** - Use full paths for scripts2312</Warning>
1096 2313
1097### Advanced Debugging2314### Security best practices
1098 2315
1099For complex hook issues:2316Keep these practices in mind when writing hooks:
1100 2317
11011. **Inspect hook execution** - Use `claude --debug` to see detailed hook2318* **Validate and sanitize inputs**: never trust input data blindly
1102 execution2319* **Always quote shell variables**: use `"$VAR"` not `$VAR`
11032. **Validate JSON schemas** - Test hook input/output with external tools2320* **Block path traversal**: check for `..` in file paths
11043. **Check environment variables** - Verify Claude Code's environment is correct2321* **Use absolute paths**: specify full paths for scripts, using `"$CLAUDE_PROJECT_DIR"` for the project root
11054. **Test edge cases** - Try hooks with unusual file paths or inputs2322* **Skip sensitive files**: avoid `.env`, `.git/`, keys, etc.
11065. **Monitor system resources** - Check for resource exhaustion during hook
1107 execution
11086. **Use structured logging** - Implement logging in your hook scripts
1109 2323
1110### Debug Output Example2324## Windows PowerShell tool
1111 2325
1112Use `claude --debug` to see hook execution details:2326On 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` (PowerShell 7+) with a fallback to `powershell.exe` (5.1).
1113 2327
2328```json theme={null}
2329{
2330 "hooks": {
2331 "PostToolUse": [
2332 {
2333 "matcher": "Write",
2334 "hooks": [
2335 {
2336 "type": "command",
2337 "shell": "powershell",
2338 "command": "Write-Host 'File written'"
2339 }
2340 ]
2341 }
2342 ]
2343 }
2344}
1114```2345```
2346
2347## Debug hooks
2348
2349Run `claude --debug` to see hook execution details, including which hooks matched, their exit codes, and output.
2350
2351```text theme={null}
1115[DEBUG] Executing hooks for PostToolUse:Write2352[DEBUG] Executing hooks for PostToolUse:Write
1116[DEBUG] Getting matching hook commands for PostToolUse with query: Write
1117[DEBUG] Found 1 hook matchers in settings
1118[DEBUG] Matched 1 hooks for query "Write"
1119[DEBUG] Found 1 hook commands to execute2353[DEBUG] Found 1 hook commands to execute
1120[DEBUG] Executing hook command: <Your command> with timeout 60000ms2354[DEBUG] Executing hook command: <Your command> with timeout 600000ms
1121[DEBUG] Hook command completed with status 0: <Your stdout>2355[DEBUG] Hook command completed with status 0: <Your stdout>
1122```2356```
1123 2357
1124Progress messages appear in transcript mode (Ctrl-R) showing:2358For 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.
1125 2359
1126* Which hook is running2360For troubleshooting common issues like hooks not firing, infinite Stop hook loops, or configuration errors, see [Limitations and troubleshooting](/en/hooks-guide#limitations-and-troubleshooting) in the guide.
1127* Command being executed
1128* Success/failure status
1129* Output or error messages