Automate actions with hooks changedhooks-guide
Nearest release: v2.1.307, published 3 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 22 Oct 2026 00:14 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 22 Oct 2026 18:07 UTC.
Upstream edited
Recorded here
Lines+83added
Lines−83removed
From line
6
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits45to this page, all time
The whole hunk
from line 6, old and new numbered
/
from line 6
66
77For decisions that require judgment rather than deterministic rules, you can also use [prompt-based hooks](#prompt-based-hooks) or [agent-based hooks](#agent-based-hooks) that use a Claude model to evaluate conditions.
88
9For other ways to extend Claude Code, see [skills](/docs/en/skills) for giving Claude additional instructions and executable commands, [subagents](/docs/en/sub-agents) for running tasks in isolated contexts, and [plugins](/docs/en/plugins/overview) for packaging extensions to share across projects.
9For other ways to extend Claude Code, see [skills](/docs/en/skills.md) for giving Claude additional instructions and executable commands, [subagents](/docs/en/sub-agents.md) for running tasks in isolated contexts, and [plugins](/docs/en/plugins/overview.md) for packaging extensions to share across projects.
1010
1111> [!TIP]
1212>
13> This guide covers common use cases and how to get started. For full event schemas, JSON input/output formats, and advanced features like async hooks and MCP tool hooks, see the [Hooks reference](/docs/en/hooks).
13> This guide covers common use cases and how to get started. For full event schemas, JSON input/output formats, and advanced features like async hooks and MCP tool hooks, see the [Hooks reference](/docs/en/hooks.md).
1414
1515## Set up your first hook
1616
from line 71
7171
7272## What you can automate
7373
74Hooks let you run code at key points in Claude Code's lifecycle: format files after edits, block commands before they execute, send notifications when Claude needs input, inject context at session start, and more. For the full list of hook events, see the [Hooks reference](/docs/en/hooks#hook-lifecycle).
74Hooks let you run code at key points in Claude Code's lifecycle: format files after edits, block commands before they execute, send notifications when Claude needs input, inject context at session start, and more. For the full list of hook events, see the [Hooks reference](/docs/en/hooks.md#hook-lifecycle).
7575
7676Each example includes a ready-to-use configuration block that you add to a [settings file](#configure-hook-location).
7777
from line 81
8181
8282Get a desktop notification whenever Claude finishes working and needs your input, so you can switch to other tasks without checking the terminal.
8383
84This hook uses the `Notification` event, which Claude Code fires when Claude is waiting for input or permission. See [when each notification type fires](/docs/en/hooks#notification) for the exact timing.
84This hook uses the `Notification` event, which Claude Code fires when Claude is waiting for input or permission. See [when each notification type fires](/docs/en/hooks.md#notification) for the exact timing.
8585
8686Each tab below uses the platform's native notification command. Add this to `~/.claude/settings.json`:
8787
from line 177
177177
178178| Matcher | Fires when |
179179| :- | :- |
180| `permission_prompt` | Claude needs you to approve a tool use or a sandboxed command's [network request](/docs/en/sandboxing#network-isolation), and the prompt has waited about six seconds |
180| `permission_prompt` | Claude needs you to approve a tool use or a sandboxed command's [network request](/docs/en/sandboxing.md#network-isolation), and the prompt has waited about six seconds |
181181| `idle_prompt` | Claude finished responding about 60 seconds ago and you haven't typed since |
182182| `auth_success` | Authentication completes |
183183| `elicitation_dialog` | An MCP server opens an elicitation form and you haven't typed for about six seconds |
184184| `elicitation_url_dialog` | An MCP server asks you to open a browser URL and you haven't typed for about six seconds |
185| `elicitation_complete` | An MCP server reports that a [URL-mode elicitation](/docs/en/hooks#elicitation-input) is complete |
185| `elicitation_complete` | An MCP server reports that a [URL-mode elicitation](/docs/en/hooks.md#elicitation-input) is complete |
186186| `elicitation_response` | An MCP elicitation response is sent back to the server |
187| `agent_needs_input` | A background session starts waiting on your input while [agent view](/docs/en/agent-view) is open. Also fires when a terminal session shows you an [agent team teammate's terminal setup question](/docs/en/agent-teams#choose-a-display-mode) or auto mode's notice about [classifier request charges](/docs/en/auto-mode-classifier-billing) and you haven't typed for about six seconds |
188| `agent_completed` | A background session finishes or fails. Fires only while [agent view](/docs/en/agent-view) is open |
189| `quota_auto_resume_fired` | Claude Code continues your task after a claude.ai usage limit paused it: at the reset, or sooner when something you do in Claude Code during the wait, such as adding usage credits, upgrading your plan, or switching models, makes usage available again, with the [model-setting exception](/docs/en/interactive-mode#wait-for-a-usage-limit-to-reset) |
187| `agent_needs_input` | A background session starts waiting on your input while [agent view](/docs/en/agent-view.md) is open. Also fires when a terminal session shows you an [agent team teammate's terminal setup question](/docs/en/agent-teams.md#choose-a-display-mode) or auto mode's notice about [classifier request charges](/docs/en/auto-mode-classifier-billing.md) and you haven't typed for about six seconds |
188| `agent_completed` | A background session finishes or fails. Fires only while [agent view](/docs/en/agent-view.md) is open |
189| `quota_auto_resume_fired` | Claude Code continues your task after a claude.ai usage limit paused it: at the reset, or sooner when something you do in Claude Code during the wait, such as adding usage credits, upgrading your plan, or switching models, makes usage available again, with the [model-setting exception](/docs/en/interactive-mode.md#wait-for-a-usage-limit-to-reset) |
190190| `quota_auto_resume_stale` | A claude.ai usage limit reset while your computer slept for more than about 30 minutes. Claude Code waits for you to press `Enter` instead of continuing. After a shorter sleep it continues and fires `quota_auto_resume_fired` instead |
191191| `quota_auto_resume_disabled` | Claude Code ends its wait for a claude.ai usage limit without continuing your task: [`autoContinueAtUsageLimit`](/docs/en/settings-reference#autocontinueatusagelimit) turned off or the reset moved more than 24 hours away during a wait Claude Code started on its own, the continued task kept hitting the limit, or the continuation was blocked before it reached the model. Doesn't fire when you press `Esc` or `Ctrl+C`, or pick **Don't continue automatically** |
192| `plugin_notification` | A [mod](/docs/en/plugins/mods/api#show-something-without-starting-a-turn) raises a notification with `$.ui.notify`. The hook's `title` is the mod's name unless the mod set one |
192| `plugin_notification` | A [mod](/docs/en/plugins/mods/api.md#show-something-without-starting-a-turn) raises a notification with `$.ui.notify`. The hook's `title` is the mod's name unless the mod set one |
193193
194Claude Code times `permission_prompt` differently in a terminal and in Claude Desktop, the VS Code extension, and other hosts that answer permission requests through the Agent SDK. See [when each notification type fires](/docs/en/hooks#notification) for both timings.
194Claude Code times `permission_prompt` differently in a terminal and in Claude Desktop, the VS Code extension, and other hosts that answer permission requests through the Agent SDK. See [when each notification type fires](/docs/en/hooks.md#notification) for both timings.
195195
196196The `quota_auto_resume_fired`, `quota_auto_resume_stale`, and `quota_auto_resume_disabled` matchers require Claude Code v2.1.234 or later.
197197
from line 229
229229
230230When the hook succeeds, Claude Code shows nothing in the conversation. To confirm the hook ran, check that the edited file is reformatted, or see [Check what a hook did](#check-what-a-hook-did).
231231
232To reformat a specific file however it changes, including when a `Bash` command rewrites it, use a [FileChanged](/docs/en/hooks#filechanged) hook instead.
232To reformat a specific file however it changes, including when a `Bash` command rewrites it, use a [FileChanged](/docs/en/hooks.md#filechanged) hook instead.
233233
234234> [!NOTE]
235235>
from line 325
325325}
326326```
327327
328You can replace the `echo` with any command that produces dynamic output, like `git log --oneline -5` to show recent commits. For injecting context on every session start, consider using [CLAUDE.md](/docs/en/memory) instead. For environment variables, see [`CLAUDE_ENV_FILE`](/docs/en/hooks#persist-environment-variables) in the reference.
328You can replace the `echo` with any command that produces dynamic output, like `git log --oneline -5` to show recent commits. For injecting context on every session start, consider using [CLAUDE.md](/docs/en/memory.md) instead. For environment variables, see [`CLAUDE_ENV_FILE`](/docs/en/hooks#persist-environment-variables) in the reference.
329329
330330### Audit configuration changes
331331
from line 351
351351}
352352```
353353
354The matcher filters by configuration type: `user_settings`, `project_settings`, `local_settings`, `policy_settings`, or `skills`. To block a change from taking effect, exit with code 2 or return `{"decision": "block"}`. See the [ConfigChange reference](/docs/en/hooks#configchange) for the full input schema.
354The matcher filters by configuration type: `user_settings`, `project_settings`, `local_settings`, `policy_settings`, or `skills`. To block a change from taking effect, exit with code 2 or return `{"decision": "block"}`. See the [ConfigChange reference](/docs/en/hooks.md#configchange) for the full input schema.
355355
356356To confirm the hook records changes, edit a settings file in another editor while a session is running, then open `~/claude-config-audit.log`: the hook appends one JSON line per change with the timestamp, source, and file path.
357357
from line 390
390390
391391Run `direnv allow` once in each directory that has an `.envrc` so direnv is permitted to load it. If you use devbox or nix instead of direnv, the same pattern works with `devbox shellenv` or `devbox global shellenv` in place of `direnv export bash`.
392392
393To react to specific files instead of every directory change, use `FileChanged` with a `matcher` listing the filenames to watch, separated by `|`. When building the watch list, Claude Code splits this value into literal filenames rather than evaluating it as a regex. See [FileChanged](/docs/en/hooks#filechanged) for how the same value also filters which hook groups run when a file changes. This example watches `.envrc` and `.env` in the working directory:
393To react to specific files instead of every directory change, use `FileChanged` with a `matcher` listing the filenames to watch, separated by `|`. When building the watch list, Claude Code splits this value into literal filenames rather than evaluating it as a regex. See [FileChanged](/docs/en/hooks.md#filechanged) for how the same value also filters which hook groups run when a file changes. This example watches `.envrc` and `.env` in the working directory:
394394
395395```json theme={null}
396396{
from line 410
410410}
411411```
412412
413See the [CwdChanged](/docs/en/hooks#cwdchanged) and [FileChanged](/docs/en/hooks#filechanged) reference entries for input schemas, `watchPaths` output, and `CLAUDE_ENV_FILE` details.
413See the [CwdChanged](/docs/en/hooks.md#cwdchanged) and [FileChanged](/docs/en/hooks.md#filechanged) reference entries for input schemas, `watchPaths` output, and `CLAUDE_ENV_FILE` details.
414414
415415### Auto-approve specific permission prompts
416416
from line 444
444444
445445> [!NOTE]
446446>
447> `bypassPermissions` only applies if you started the session with bypass mode already available: `--dangerously-skip-permissions`, `--permission-mode bypassPermissions`, `--allow-dangerously-skip-permissions`, or `permissions.defaultMode: "bypassPermissions"` in [user, `--settings`, or managed settings](/docs/en/settings-reference#permissions-defaultmode). It doesn't apply if bypass mode is disabled by [`permissions.disableBypassPermissionsMode`](/docs/en/permissions#managed-settings), or if you started the session in [restricted mode](/docs/en/cli-reference#cli-flags).
447> `bypassPermissions` only applies if you started the session with bypass mode already available: `--dangerously-skip-permissions`, `--permission-mode bypassPermissions`, `--allow-dangerously-skip-permissions`, or `permissions.defaultMode: "bypassPermissions"` in [user, `--settings`, or managed settings](/docs/en/settings-reference#permissions-defaultmode). It doesn't apply if bypass mode is disabled by [`permissions.disableBypassPermissionsMode`](/docs/en/permissions#managed-settings), or if you started the session in [restricted mode](/docs/en/cli-reference.md#cli-flags).
448448>
449449> Claude Code never saves it as `defaultMode`.
450450
from line 464
464464}
465465```
466466
467Keep the matcher as narrow as possible. Matching on `.*` or leaving the matcher empty would auto-approve every tool permission prompt, including file writes and shell commands. See the [PermissionRequest reference](/docs/en/hooks#permissionrequest-decision-control) for the full set of decision fields.
467Keep the matcher as narrow as possible. Matching on `.*` or leaving the matcher empty would auto-approve every tool permission prompt, including file writes and shell commands. See the [PermissionRequest reference](/docs/en/hooks.md#permissionrequest-decision-control) for the full set of decision fields.
468468
469469## How hooks work
470470
471Claude Code fires hook events at specific points in its lifecycle. When an event fires, Claude Code runs the matching hooks in parallel; see [Hook handler fields](/docs/en/hooks#hook-handler-fields) for how duplicate handlers are treated. The table below shows each event and when it triggers:
471Claude Code fires hook events at specific points in its lifecycle. When an event fires, Claude Code runs the matching hooks in parallel; see [Hook handler fields](/docs/en/hooks.md#hook-handler-fields) for how duplicate handlers are treated. The table below shows each event and when it triggers:
472472
473473| Event | When it fires |
474474| :- | :- |
475475| `SessionStart` | When a session begins or resumes |
476476| `Setup` | When you start Claude Code with `--init-only`, or with `--init` or `--maintenance` in `-p` mode. For one-time preparation in CI or scripts |
477| `UserPromptSubmit` | When a prompt is submitted, before Claude processes it. Also fires on [turns Claude Code starts on its own](/docs/en/hooks#userpromptsubmit) |
477| `UserPromptSubmit` | When a prompt is submitted, before Claude processes it. Also fires on [turns Claude Code starts on its own](/docs/en/hooks.md#userpromptsubmit) |
478478| `UserPromptExpansion` | When a user-typed command expands into a prompt, before it reaches Claude. Can block the expansion |
479479| `PreToolUse` | Before a tool call executes. Can block it |
480480| `PermissionRequest` | When a tool call needs a permission decision |
from line 490
490490| `TaskCompleted` | When a task is being marked as completed |
491491| `Stop` | When Claude finishes responding |
492492| `StopFailure` | When the turn ends due to an API error |
493| `TeammateIdle` | When an [agent team](/docs/en/agent-teams) teammate is about to go idle |
493| `TeammateIdle` | When an [agent team](/docs/en/agent-teams.md) teammate is about to go idle |
494494| `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 |
495495| `ConfigChange` | When a configuration file changes during a session |
496496| `CwdChanged` | When the working directory changes, for example when Claude executes a `cd` command. Useful for reactive environment management with tools like direnv |
from line 509
509509Each hook has a `type` that determines how it runs. Most hooks use `"type": "command"`, which runs a shell command. Four other types are available:
510510
511511* `"type": "http"`: POST event data to a URL. See [HTTP hooks](#http-hooks).
512* `"type": "mcp_tool"`: call a tool on a configured MCP server. See [MCP tool hooks](/docs/en/hooks#mcp-tool-hook-fields).
512* `"type": "mcp_tool"`: call a tool on a configured MCP server. See [MCP tool hooks](/docs/en/hooks.md#mcp-tool-hook-fields).
513513* `"type": "prompt"`: single-turn LLM evaluation. See [Prompt-based hooks](#prompt-based-hooks).
514514* `"type": "agent"`: multi-turn verification with tool access. Agent hooks are experimental and may change. See [Agent-based hooks](#agent-based-hooks).
515515
from line 571
571571}
572572```
573573
574Your script can parse that JSON and act on any of those fields. `UserPromptSubmit` hooks get the `prompt` text instead, `SessionStart` hooks get a `source` of `startup`, `resume`, `clear`, `compact`, or `fork`, and so on. See [Common input fields](/docs/en/hooks#common-input-fields) in the reference for shared fields, and each event's section for event-specific schemas.
574Your script can parse that JSON and act on any of those fields. `UserPromptSubmit` hooks get the `prompt` text instead, `SessionStart` hooks get a `source` of `startup`, `resume`, `clear`, `compact`, or `fork`, and so on. See [Common input fields](/docs/en/hooks.md#common-input-fields) in the reference for shared fields, and each event's section for event-specific schemas.
575575
576576#### Hook output
577577
from line 590
590590exit 0 # exit 0 = no decision; the normal permission flow applies
591591```
592592
593With an exit code besides 0 and 2 and no valid JSON output, the action proceeds on most events, unless the hook is one [whose failure blocks the action](/docs/en/hooks#block-the-action-when-a-hook-fails). A hook with `onFailure` set to `"block"` is one example. Exit 0 to report no objection, or exit 2 to block the action:
593With an exit code besides 0 and 2 and no valid JSON output, the action proceeds on most events, unless the hook is one [whose failure blocks the action](/docs/en/hooks.md#block-the-action-when-a-hook-fails). A hook with `onFailure` set to `"block"` is one example. Exit 0 to report no objection, or exit 2 to block the action:
594594
595* **Exit 0**: if you exit 0 from a `PreToolUse` hook, the tool call still goes through the normal [permission flow](/docs/en/permissions), because exit 0 doesn't approve it. For `UserPromptSubmit`, `UserPromptExpansion`, `SessionStart`, and `PostModelSwitch` hooks, Claude Code adds stdout it [treats as plain text](/docs/en/hooks#exit-code-0) to Claude's context.
596* **Exit 2**: write a reason to stderr. Depending on the event, Claude receives it as feedback so it can adjust, the user sees it, or no message appears, as with `ConfigChange` and `Elicitation`. Some events can't be blocked: for `SessionStart` and others, exit 2 shows stderr to the user and execution continues. See [exit code 2 behavior per event](/docs/en/hooks#exit-code-2-behavior-per-event) for the full list.
597* **Any other exit code**: for the per-event exceptions, such as `WorktreeCreate` failing on any nonzero exit, see [Exit code output](/docs/en/hooks#exit-code-output). On other events, unless the hook is one whose failure blocks the action, find your case by what your hook printed to stdout:
595* **Exit 0**: if you exit 0 from a `PreToolUse` hook, the tool call still goes through the normal [permission flow](/docs/en/permissions.md), because exit 0 doesn't approve it. For `UserPromptSubmit`, `UserPromptExpansion`, `SessionStart`, and `PostModelSwitch` hooks, Claude Code adds stdout it [treats as plain text](/docs/en/hooks.md#exit-code-0) to Claude's context.
596* **Exit 2**: write a reason to stderr. Depending on the event, Claude receives it as feedback so it can adjust, the user sees it, or no message appears, as with `ConfigChange` and `Elicitation`. Some events can't be blocked: for `SessionStart` and others, exit 2 shows stderr to the user and execution continues. See [exit code 2 behavior per event](/docs/en/hooks.md#exit-code-2-behavior-per-event) for the full list.
597* **Any other exit code**: for the per-event exceptions, such as `WorktreeCreate` failing on any nonzero exit, see [Exit code output](/docs/en/hooks.md#exit-code-output). On other events, unless the hook is one whose failure blocks the action, find your case by what your hook printed to stdout:
598598 * A parsed object that passes schema validation: Claude Code ignores the exit code, the JSON alone decides the outcome, and the hook isn't reported as an error.
599 * A parsed object that fails schema validation, or stdout that Claude Code [tries to parse as JSON](/docs/en/hooks#exit-code-0) but that isn't valid JSON: the action proceeds as a non-blocking error. Check the `<hook name> hook error` notice for the validation or parse message.
600 * Stdout that Claude Code [treats as plain text](/docs/en/hooks#exit-code-0), or empty stdout: the action proceeds as a non-blocking error. The transcript shows a `<hook name> hook error` notice, then the first line of stderr prefixed with `Failed with non-blocking status code:`. To capture the full stderr, enable [debug logging](/docs/en/hooks#debug-hooks) with `claude --debug` or by running `/debug` mid-session.
599 * A parsed object that fails schema validation, or stdout that Claude Code [tries to parse as JSON](/docs/en/hooks.md#exit-code-0) but that isn't valid JSON: the action proceeds as a non-blocking error. Check the `<hook name> hook error` notice for the validation or parse message.
600 * Stdout that Claude Code [treats as plain text](/docs/en/hooks.md#exit-code-0), or empty stdout: the action proceeds as a non-blocking error. The transcript shows a `<hook name> hook error` notice, then the first line of stderr prefixed with `Failed with non-blocking status code:`. To capture the full stderr, enable [debug logging](/docs/en/hooks.md#debug-hooks) with `claude --debug` or by running `/debug` mid-session.
601601
602602#### Structured JSON output
603603
from line 605
605605
606606> [!NOTE]
607607>
608> Use exit 2 to block with a stderr message, or exit 0 with JSON for structured control. Choose one approach per hook. For what happens when you mix them, see [Exit code output](/docs/en/hooks#exit-code-output).
608> Use exit 2 to block with a stderr message, or exit 0 with JSON for structured control. Choose one approach per hook. For what happens when you mix them, see [Exit code output](/docs/en/hooks.md#exit-code-output).
609609
610610For example, a `PreToolUse` hook can deny a tool call and tell Claude why, or escalate it to the user for approval:
611611
from line 623
623623
624624On `PreToolUse`, you can also return `"ask"` to show the user the normal permission prompt, or `"allow"` to skip the interactive permission prompt. With `"allow"`, deny and ask rules, including enterprise managed deny lists, still apply, as do prompts for:
625625
626* Reads from [network paths](/docs/en/permissions#network-paths)
626* Reads from [network paths](/docs/en/permissions.md#network-paths)
627627* MCP tools marked [`requiresUserInteraction`](/docs/en/mcp#require-approval-for-a-specific-tool)
628628* Connector tools [your organization set to `ask`](/docs/en/mcp#organization-controls-on-connector-tools) in sessions where that setting reaches Claude Code
629629
630For what `"allow"` does in plan mode, see [Hook approvals in plan mode](/docs/en/permissions#hook-approvals-in-plan-mode).
630For what `"allow"` does in plan mode, see [Hook approvals in plan mode](/docs/en/permissions.md#hook-approvals-in-plan-mode).
631631
632A fourth value, `"defer"`, is available in [non-interactive mode](/docs/en/headless) with the `-p` flag. Returning it ends the turn with the tool call preserved, so an Agent SDK wrapper can collect input and resume. See [Defer a tool call for later](/docs/en/hooks#defer-a-tool-call-for-later) in the reference.
632A fourth value, `"defer"`, is available in [non-interactive mode](/docs/en/headless.md) with the `-p` flag. Returning it ends the turn with the tool call preserved, so an Agent SDK wrapper can collect input and resume. See [Defer a tool call for later](/docs/en/hooks.md#defer-a-tool-call-for-later) in the reference.
633633
634A `PreModelSwitch` hook returns the same `permissionDecision` field: `"allow"` lets a model switch proceed, and `"deny"` cancels it. `"ask"` has you confirm the switch when you run `/model` in an interactive session; everywhere else, Claude Code treats `"ask"` as a refusal. See [PreModelSwitch decision control](/docs/en/hooks#premodelswitch-decision-control).
634A `PreModelSwitch` hook returns the same `permissionDecision` field: `"allow"` lets a model switch proceed, and `"deny"` cancels it. `"ask"` has you confirm the switch when you run `/model` in an interactive session; everywhere else, Claude Code treats `"ask"` as a refusal. See [PreModelSwitch decision control](/docs/en/hooks.md#premodelswitch-decision-control).
635635
636Other events use different decision patterns. For example, `PostToolUse` and `Stop` hooks use a top-level `decision: "block"` field, while `PermissionRequest` uses `hookSpecificOutput.decision.behavior`. See the [summary table](/docs/en/hooks#decision-control) in the reference for a full breakdown by event.
636Other events use different decision patterns. For example, `PostToolUse` and `Stop` hooks use a top-level `decision: "block"` field, while `PermissionRequest` uses `hookSpecificOutput.decision.behavior`. See the [summary table](/docs/en/hooks.md#decision-control) in the reference for a full breakdown by event.
637637
638638For `UserPromptSubmit` hooks, use `hookSpecificOutput.additionalContext` instead to inject text into Claude's context. Nest `additionalContext` inside `hookSpecificOutput`; if you place it at the top level of the JSON, Claude Code silently ignores it. For example, this output adds the current branch state to every prompt:
639639
from line 646
646646}
647647```
648648
649See [UserPromptSubmit decision control](/docs/en/hooks#userpromptsubmit-decision-control) for the full output shape, including blocking prompts and setting the session title.
649See [UserPromptSubmit decision control](/docs/en/hooks.md#userpromptsubmit-decision-control) for the full output shape, including blocking prompts and setting the session title.
650650
651651Hooks with `type: "prompt"` handle output differently: see [Prompt-based hooks](#prompt-based-hooks).
652652
from line 669
669669}
670670```
671671
672The `"Edit|Write"` matcher fires only when Claude uses the `Edit` or `Write` tool, not when it uses `Bash`, `Read`, or any other tool. A comma separates alternatives the same way, so `"Edit, Write"` is equivalent. See [Matcher patterns](/docs/en/hooks#matcher-patterns) for how plain names and regular expressions are evaluated.
672The `"Edit|Write"` matcher fires only when Claude uses the `Edit` or `Write` tool, not when it uses `Bash`, `Read`, or any other tool. A comma separates alternatives the same way, so `"Edit, Write"` is equivalent. See [Matcher patterns](/docs/en/hooks.md#matcher-patterns) for how plain names and regular expressions are evaluated.
673673
674674> [!NOTE]
675675>
676> Claude can also create or modify files by running shell commands. If your hook must see every file change, such as for compliance scanning or audit logging, add a [`Stop`](/docs/en/hooks#stop) hook that scans the working tree once per turn. For per-call coverage instead, also match `Bash|PowerShell` and have your script list modified and untracked files with `git status --porcelain`. The [PowerShell hook input section](/docs/en/hooks#powershell) explains why matching `Bash` alone is not enough. To run a hook when a specific file changes on disk, whatever wrote it, use a [FileChanged](/docs/en/hooks#filechanged) hook.
676> Claude can also create or modify files by running shell commands. If your hook must see every file change, such as for compliance scanning or audit logging, add a [`Stop`](/docs/en/hooks#stop) hook that scans the working tree once per turn. For per-call coverage instead, also match `Bash|PowerShell` and have your script list modified and untracked files with `git status --porcelain`. The [PowerShell hook input section](/docs/en/hooks.md#powershell) explains why matching `Bash` alone is not enough. To run a hook when a specific file changes on disk, whatever wrote it, use a [FileChanged](/docs/en/hooks.md#filechanged) hook.
677677
678678Each event type matches on a specific field:
679679
from line 686
686686| `Notification` | notification type | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`, `elicitation_url_dialog`, `elicitation_complete`, `elicitation_response`, `agent_needs_input`, `agent_completed`, `quota_auto_resume_fired`, `quota_auto_resume_stale`, `quota_auto_resume_disabled`, `plugin_notification` |
687687| `SubagentStart` | agent type | `general-purpose`, `Explore`, `Plan`, or custom agent names |
688688| `PreCompact`, `PostCompact` | what triggered compaction | `manual`, `auto` |
689| `PreModelSwitch`, `PostModelSwitch` | canonical name of the model the session switches to, as described under [PreModelSwitch](/docs/en/hooks#premodelswitch) | `claude-opus-5`, `claude-opus-4-6\|claude-opus-5`, `.*opus.*` |
689| `PreModelSwitch`, `PostModelSwitch` | canonical name of the model the session switches to, as described under [PreModelSwitch](/docs/en/hooks.md#premodelswitch) | `claude-opus-5`, `claude-opus-4-6\|claude-opus-5`, `.*opus.*` |
690690| `SubagentStop` | agent type | same values as `SubagentStart` |
691691| `ConfigChange` | configuration source | `user_settings`, `project_settings`, `local_settings`, `policy_settings`, `skills` |
692692| `DirectoryAdded` | how the directory was added | `slash_command`, `register_repo_root` |
from line 694
694694| `InstructionsLoaded` | load reason | `session_start`, `nested_traversal`, `path_glob_match`, `include`, `compact` |
695695| `Elicitation` | MCP server name | your configured MCP server names |
696696| `ElicitationResult` | MCP server name | same values as `Elicitation` |
697| `FileChanged` | literal filenames to watch (see [FileChanged](/docs/en/hooks#filechanged)) | `.envrc\|.env` |
697| `FileChanged` | literal filenames to watch (see [FileChanged](/docs/en/hooks.md#filechanged)) | `.envrc\|.env` |
698698| `UserPromptExpansion` | command name | your skill or command names |
699699| `UserPromptSubmit`, `PostToolBatch`, `Stop`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove`, `CwdChanged`, `MessageDisplay` | no matcher support | always fires on every occurrence |
700700
from line 724
724724
725725**Match MCP tools**
726726
727MCP tools use a different naming convention than built-in tools: `mcp__<server>__<tool>`, where `<server>` is the MCP server name and `<tool>` is the tool it provides. For example, `mcp__github__search_repositories` or `mcp__filesystem__read_file`. Tools from a [plugin-bundled server](/docs/en/mcp#plugin-provided-mcp-servers) use a scoped server segment instead, such as `mcp__plugin_my-plugin_db__query`. Use a regex matcher to target all tools from a specific server, or match across servers with a pattern like `mcp__.*__write.*`. See [Match MCP tools](/docs/en/hooks#match-mcp-tools) in the reference for the full list of examples.
727MCP tools use a different naming convention than built-in tools: `mcp__<server>__<tool>`, where `<server>` is the MCP server name and `<tool>` is the tool it provides. For example, `mcp__github__search_repositories` or `mcp__filesystem__read_file`. Tools from a [plugin-bundled server](/docs/en/mcp.md#plugin-provided-mcp-servers) use a scoped server segment instead, such as `mcp__plugin_my-plugin_db__query`. Use a regex matcher to target all tools from a specific server, or match across servers with a pattern like `mcp__.*__write.*`. See [Match MCP tools](/docs/en/hooks.md#match-mcp-tools) in the reference for the full list of examples.
728728
729The command below extracts the tool name from the hook's JSON input with `jq` and writes it to stderr. Writing to stderr keeps stdout clean for JSON output and sends the message to the [debug log](/docs/en/hooks#debug-hooks):
729The command below extracts the tool name from the hook's JSON input with `jq` and writes it to stderr. Writing to stderr keeps stdout clean for JSON output and sends the message to the [debug log](/docs/en/hooks.md#debug-hooks):
730730
731731```json theme={null}
732732{
from line 770
770770
771771#### Filter by tool name and arguments with the `if` field
772772
773The `if` field uses [permission rule syntax](/docs/en/permissions) to filter hooks by tool name and arguments together, so the hook process only spawns when the tool call matches. This goes beyond `matcher`, which filters at the group level by tool name only.
773The `if` field uses [permission rule syntax](/docs/en/permissions.md) to filter hooks by tool name and arguments together, so the hook process only spawns when the tool call matches. This goes beyond `matcher`, which filters at the group level by tool name only.
774774
775775For example, this configuration runs a hook only when Claude uses `git` commands rather than all Bash commands:
776776
from line 803
803803| `Bash(git *)` | `echo $(date)` | no | no subcommand matches `git *` |
804804| `Bash(git push *)` | `echo $(date)` | yes | patterns that specify more than the command name run the hook anyway on `$()`, backticks, or `$VAR` |
805805
806When Claude Code can't determine which commands the Bash input runs, it runs your hook regardless of the pattern. The [Bash matching table](/docs/en/hooks#bash-if-matching) covers the command shapes Claude Code can and can't narrow by subcommand. Because the filter is best-effort, use the [permission system](/docs/en/permissions) rather than a hook to enforce a hard allow or deny.
806When Claude Code can't determine which commands the Bash input runs, it runs your hook regardless of the pattern. The [Bash matching table](/docs/en/hooks.md#bash-if-matching) covers the command shapes Claude Code can and can't narrow by subcommand. Because the filter is best-effort, use the [permission system](/docs/en/permissions.md) rather than a hook to enforce a hard allow or deny.
807807
808808The `if` field accepts the same patterns as permission rules: `"Bash(git *)"`, `"Edit(*.ts)"`, and so on. To match multiple tool names, use separate handlers each with its own `if` value, or match at the `matcher` level where pipe alternation is supported.
809809
from line 819
819819| `.claude/settings.json` | Single project | Yes, can be committed to the repo |
820820| `.claude/settings.local.json` | Single project | No, gitignored when Claude Code saves a setting to it |
821821| Managed policy settings | Organization-wide | Yes, admin-controlled |
822| [Plugin](/docs/en/plugins/overview) `hooks/hooks.json` | When plugin is enabled | Yes, bundled with the plugin |
823| [Skill](/docs/en/skills) frontmatter | The rest of the session once the skill is invoked. See [Hooks in skills and agents](/docs/en/hooks#hooks-in-skills-and-agents) | Yes, defined in the skill file |
824| [Subagent](/docs/en/sub-agents) frontmatter | While that subagent is running | Yes, defined in the subagent file |
822| [Plugin](/docs/en/plugins/overview.md) `hooks/hooks.json` | When plugin is enabled | Yes, bundled with the plugin |
823| [Skill](/docs/en/skills.md) frontmatter | The rest of the session once the skill is invoked. See [Hooks in skills and agents](/docs/en/hooks.md#hooks-in-skills-and-agents) | Yes, defined in the skill file |
824| [Subagent](/docs/en/sub-agents.md) frontmatter | While that subagent is running | Yes, defined in the subagent file |
825825
826826Run [`/hooks`](/docs/en/hooks#the-%2Fhooks-menu) in Claude Code to browse all configured hooks grouped by event.
827827
828To disable hooks, set `"disableAllHooks": true` in your settings file. Claude Code reads the value left after [settings precedence](/docs/en/hooks#disable-or-remove-hooks) applies, so a project's settings file can override yours. Hooks configured in managed settings still run unless `disableAllHooks` is also set there. For the full reach of each level, see [`disableAllHooks`](/docs/en/settings-reference#disableallhooks).
828To disable hooks, set `"disableAllHooks": true` in your settings file. Claude Code reads the value left after [settings precedence](/docs/en/hooks.md#disable-or-remove-hooks) applies, so a project's settings file can override yours. Hooks configured in managed settings still run unless `disableAllHooks` is also set there. For the full reach of each level, see [`disableAllHooks`](/docs/en/settings-reference#disableallhooks).
829829
830830If you edit settings files directly while Claude Code is running, the file watcher normally picks up hook changes automatically.
831831
from line 861
861861}
862862```
863863
864For full configuration options, see [Prompt-based hooks](/docs/en/hooks#prompt-based-hooks) in the reference.
864For full configuration options, see [Prompt-based hooks](/docs/en/hooks.md#prompt-based-hooks) in the reference.
865865
866866## Agent-based hooks
867867
868868> [!WARNING]
869869>
870> Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](/docs/en/hooks#command-hook-fields).
870> Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](/docs/en/hooks.md#command-hook-fields).
871871
872872When verification requires inspecting files or running commands, use `type: "agent"` hooks. Unlike prompt hooks, which make a single LLM call, agent hooks spawn a subagent that can read files, search code, and use other tools to verify conditions before returning a decision.
873873
874Agent hooks use the `"ok"` / `"reason"` response format with a longer default timeout of 60 seconds and up to 50 tool-use turns. They don't support the prompt-hook `impossible` field. On `ok: false`, Claude Code handles an agent hook the way it handles a prompt hook with `continueOnBlock: true` on the same event, so on `PreToolUse` and `PostToolUse` the turn continues; agent hooks have no `continueOnBlock` field. See [agent hook configuration](/docs/en/hooks#agent-hook-configuration) for the fields, including the `$ARGUMENTS` placeholder that Claude Code replaces with the hook's JSON input.
874Agent hooks use the `"ok"` / `"reason"` response format with a longer default timeout of 60 seconds and up to 50 tool-use turns. They don't support the prompt-hook `impossible` field. On `ok: false`, Claude Code handles an agent hook the way it handles a prompt hook with `continueOnBlock: true` on the same event, so on `PreToolUse` and `PostToolUse` the turn continues; agent hooks have no `continueOnBlock` field. See [agent hook configuration](/docs/en/hooks.md#agent-hook-configuration) for the fields, including the `$ARGUMENTS` placeholder that Claude Code replaces with the hook's JSON input.
875875
876876This example verifies that tests pass before allowing Claude to stop:
877877
from line 895
895895
896896Use prompt hooks when the hook input data alone is enough to make a decision. Use agent hooks when you need to verify something against the actual state of the codebase.
897897
898For full configuration options, see [Agent-based hooks](/docs/en/hooks#agent-based-hooks) in the reference.
898For full configuration options, see [Agent-based hooks](/docs/en/hooks.md#agent-based-hooks) in the reference.
899899
900900## HTTP hooks
901901
from line 926
926926}
927927```
928928
929Your endpoint responds with a JSON body in the same [output format](/docs/en/hooks#json-output) as command hooks, and Claude Code also checks the response status:
929Your endpoint responds with a JSON body in the same [output format](/docs/en/hooks.md#json-output) as command hooks, and Claude Code also checks the response status:
930930
931931* **2xx status**: to block a tool call, return the appropriate `hookSpecificOutput` fields in the body.
932* **Any other status, or the request fails**: Claude Code reports a [non-blocking error](/docs/en/hooks#exit-code-output) and lets the action continue. To make a failed endpoint block the action, set [`onFailure: "block"`](/docs/en/hooks#block-the-action-when-a-hook-fails) on the hook.
932* **Any other status, or the request fails**: Claude Code reports a [non-blocking error](/docs/en/hooks.md#exit-code-output) and lets the action continue. To make a failed endpoint block the action, set [`onFailure: "block"`](/docs/en/hooks#block-the-action-when-a-hook-fails) on the hook.
933933
934934Header values support environment variable interpolation using `$VAR_NAME` or `${VAR_NAME}` syntax. Only variables listed in the `allowedEnvVars` array are resolved; all other `$VAR` references remain empty.
935935
936For full configuration options and response handling, see [HTTP hooks](/docs/en/hooks#http-hook-fields) in the reference.
936For full configuration options and response handling, see [HTTP hooks](/docs/en/hooks.md#http-hook-fields) in the reference.
937937
938938## Limitations and troubleshooting
939939
from line 941
941941
942942Keep these constraints in mind when designing hooks:
943943
944* Command hooks communicate through stdout, stderr, and exit codes only. They can't trigger `/` commands or tool calls. Text returned via `additionalContext` is injected as a [system reminder](/docs/en/glossary#system-reminder) that Claude reads as plain text. HTTP hooks communicate through the response body instead.
944* Command hooks communicate through stdout, stderr, and exit codes only. They can't trigger `/` commands or tool calls. Text returned via `additionalContext` is injected as a [system reminder](/docs/en/glossary.md#system-reminder) that Claude reads as plain text. HTTP hooks communicate through the response body instead.
945945* Hook timeouts vary by type. Override per hook with the `timeout` field in seconds.
946946 * `command`, `http`, `mcp_tool`: 10 minutes. Claude Code lowers this default to 30 seconds for `UserPromptSubmit`, `PreModelSwitch`, and `PostModelSwitch` hooks, and to 10 seconds for `MessageDisplay`.
947947 * `prompt`: 30 seconds.
from line 948
948948 * `agent`: 60 seconds.
949949 * [`SessionEnd`](/docs/en/hooks#sessionend) hooks of any type share a 1.5-second budget. If your settings set a longer per-hook `timeout`, Claude Code raises the budget to match, up to 60 seconds.
950950* `PostToolUse` hooks can't undo actions since the tool has already executed.
951* [`PermissionRequest`](/docs/en/hooks#permissionrequest) hooks fire when Claude Code is about to ask you for permission, or when it would otherwise auto-deny a call that can't prompt. In [non-interactive mode](/docs/en/headless) with the `-p` flag, they still run outside [`dontAsk` mode](/docs/en/permission-modes#allow-only-pre-approved-tools-with-dontask-mode), and a call that no hook decides and nothing else can answer is denied.
952* `Stop` hooks fire whenever Claude finishes responding, not only at task completion. They don't fire on user interrupts. API errors fire [StopFailure](/docs/en/hooks#stopfailure) instead.
951* [`PermissionRequest`](/docs/en/hooks#permissionrequest) hooks fire when Claude Code is about to ask you for permission, or when it would otherwise auto-deny a call that can't prompt. In [non-interactive mode](/docs/en/headless.md) with the `-p` flag, they still run outside [`dontAsk` mode](/docs/en/permission-modes#allow-only-pre-approved-tools-with-dontask-mode), and a call that no hook decides and nothing else can answer is denied.
952* `Stop` hooks fire whenever Claude finishes responding, not only at task completion. They don't fire on user interrupts. API errors fire [StopFailure](/docs/en/hooks.md#stopfailure) instead.
953953* When multiple `PreToolUse` hooks return [`updatedInput`](/docs/en/hooks#pretooluse) to rewrite a tool's arguments, the last one to finish takes effect. Since hooks run in parallel, the order is non-deterministic. Avoid having more than one hook modify the same tool's input.
954954
955955### Hooks and permission modes
from line 958
958958
959959* **What still holds over `"allow"`**: a hook returning `"allow"` doesn't bypass deny rules from settings, and it can't suppress the prompt for:
960960
961 * Reads from [network paths](/docs/en/permissions#network-paths)
961 * Reads from [network paths](/docs/en/permissions.md#network-paths)
962962 * MCP tools marked [`requiresUserInteraction`](/docs/en/mcp#require-approval-for-a-specific-tool)
963963 * Connector tools [your organization set to `ask`](/docs/en/mcp#organization-controls-on-connector-tools) in sessions where that setting reaches Claude Code
964964
965 In [plan mode](/docs/en/permissions#hook-approvals-in-plan-mode), if your `PreToolUse` hook returns `"allow"` for a file edit, you get the same prompt, block, or approval the edit would get with no hook. To lift this limit on hook approvals, [approve the plan](/docs/en/permission-modes#review-and-approve-a-plan) or switch to another permission mode.
966* **`"deny"` in every permission mode**: a hook that returns `permissionDecision: "deny"` blocks the tool even in `bypassPermissions` mode or with `--dangerously-skip-permissions`. `PreToolUse` hooks fire before any permission-mode check, in every [permission mode](/docs/en/permission-modes), including `dontAsk`. This lets you enforce policy that users can't bypass by changing their permission mode.
965 In [plan mode](/docs/en/permissions.md#hook-approvals-in-plan-mode), if your `PreToolUse` hook returns `"allow"` for a file edit, you get the same prompt, block, or approval the edit would get with no hook. To lift this limit on hook approvals, [approve the plan](/docs/en/permission-modes.md#review-and-approve-a-plan) or switch to another permission mode.
966* **`"deny"` in every permission mode**: a hook that returns `permissionDecision: "deny"` blocks the tool even in `bypassPermissions` mode or with `--dangerously-skip-permissions`. `PreToolUse` hooks fire before any permission-mode check, in every [permission mode](/docs/en/permission-modes.md), including `dontAsk`. This lets you enforce policy that users can't bypass by changing their permission mode.
967967
968968> [!NOTE]
969969>
970> If your machine has no [managed settings](/docs/en/settings#check-what-your-organization-enforces) and your session doesn't use [`/login` credentials](/docs/en/authentication#authentication-precedence) for a Team or Enterprise plan, a [mod](/docs/en/plugins/mods/overview) can approve a call your `PreToolUse` hook blocked. A session that runs on an API key or a long-lived [`CLAUDE_CODE_OAUTH_TOKEN`](/docs/en/authentication#generate-a-long-lived-token) instead isn't using that login, even when that login is stored.
970> If your machine has no [managed settings](/docs/en/settings.md#check-what-your-organization-enforces) and your session doesn't use [`/login` credentials](/docs/en/authentication#authentication-precedence) for a Team or Enterprise plan, a [mod](/docs/en/plugins/mods/overview.md) can approve a call your `PreToolUse` hook blocked. A session that runs on an API key or a long-lived [`CLAUDE_CODE_OAUTH_TOKEN`](/docs/en/authentication#generate-a-long-lived-token) instead isn't using that login, even when that login is stored.
971971>
972> In plan mode, a mod can approve such a blocked call only when it's read-only, so not a file edit. For the cases where a mod's approval doesn't get past your rules and hooks, see [Extend permissions with hooks](/docs/en/permissions#precedence-among-rules-hooks-and-mods).
972> In plan mode, a mod can approve such a blocked call only when it's read-only, so not a file edit. For the cases where a mod's approval doesn't get past your rules and hooks, see [Extend permissions with hooks](/docs/en/permissions.md#precedence-among-rules-hooks-and-mods).
973973
974974### Hook not firing
975975
from line 976
976976The hook is configured but never executes.
977977
978978* Run `/hooks` and confirm the hook appears under the correct event
979* For a hook in a skill's frontmatter, invoke the skill or ask Claude to, then run `/hooks` again. `/hooks` doesn't list hooks from a skill that hasn't been invoked in the session. If `/hooks` still doesn't list the hook, check for other causes, such as [frontmatter that didn't parse](/docs/en/skills#skill-not-triggering) or one of the [limits on skill hooks](/docs/en/hooks#limits-on-skill-hooks)
979* For a hook in a skill's frontmatter, invoke the skill or ask Claude to, then run `/hooks` again. `/hooks` doesn't list hooks from a skill that hasn't been invoked in the session. If `/hooks` still doesn't list the hook, check for other causes, such as [frontmatter that didn't parse](/docs/en/skills.md#skill-not-triggering) or one of the [limits on skill hooks](/docs/en/hooks.md#limits-on-skill-hooks)
980980* Check that the matcher pattern matches the tool name exactly. Matchers are case-sensitive
981981* Verify you're triggering the right event type: `PreToolUse` fires before tool execution, `PostToolUse` fires after. A `PermissionRequest` hook fires when Claude Code is about to ask you for permission; see the [limitations](#limitations) for the non-interactive cases
982982
from line 989
989989 echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
990990 echo $? # Check the exit code
991991 ```
992* If you see "command not found", use absolute paths or `${CLAUDE_PROJECT_DIR}` to reference scripts. To avoid shell quoting entirely, add `"args": []` to switch to [exec form](/docs/en/hooks#exec-form-and-shell-form), which spawns the script directly without a shell
992* If you see "command not found", use absolute paths or `${CLAUDE_PROJECT_DIR}` to reference scripts. To avoid shell quoting entirely, add `"args": []` to switch to [exec form](/docs/en/hooks.md#exec-form-and-shell-form), which spawns the script directly without a shell
993993* If you see "jq: command not found", install `jq` or use Python/Node.js for JSON parsing
994* If the message includes `blocking because Git Bash wasn't found`, you're on Windows, Claude Code didn't find Git Bash, and the failed or timed-out hook blocked the tool call. See [Failed hooks on Windows without Git Bash](/docs/en/hooks#failed-hooks-on-windows-without-git-bash) for the fixes
994* If the message includes `blocking because Git Bash wasn't found`, you're on Windows, Claude Code didn't find Git Bash, and the failed or timed-out hook blocked the tool call. See [Failed hooks on Windows without Git Bash](/docs/en/hooks.md#failed-hooks-on-windows-without-git-bash) for the fixes
995995* If the notice shows a JSON validation message, your hook's stdout parsed as JSON but failed schema validation. If it shows a JSON parse message, the stdout looked like a JSON object but wasn't valid JSON. Both happen even on exit 0.
996996
997 To fix a parse failure, build the payload with a JSON encoder such as `jq` instead of string concatenation, so quotes and backslashes inside values are escaped. The reference's [Exit code output](/docs/en/hooks#exit-code-output) section covers the exit-code and JSON combinations
997 To fix a parse failure, build the payload with a JSON encoder such as `jq` instead of string concatenation, so quotes and backslashes inside values are escaped. The reference's [Exit code output](/docs/en/hooks.md#exit-code-output) section covers the exit-code and JSON combinations
998998* If the script isn't running at all, make it executable: `chmod +x ./my-hook.sh`
999999
10001000### `/hooks` shows no hooks configured
from line 1028
10281028Your hook prints valid JSON, but the decision doesn't take effect and no error appears in the transcript. Check which cause applies:
10291029
10301030* **Extra output before the JSON**: something else writes to stdout first, usually an unconditional `echo` in your shell profile, so the output no longer starts with `{`. See [Shell profile output before the JSON](#shell-profile-output-before-the-json).
1031* **A field at the wrong level**: compare each field's placement against the [JSON output](/docs/en/hooks#json-output) format. For example, `permissionDecision` belongs inside `hookSpecificOutput`, not at the top level. See [Fields at the wrong level](#fields-at-the-wrong-level).
1031* **A field at the wrong level**: compare each field's placement against the [JSON output](/docs/en/hooks.md#json-output) format. For example, `permissionDecision` belongs inside `hookSpecificOutput`, not at the top level. See [Fields at the wrong level](#fields-at-the-wrong-level).
10321032
10331033#### Shell profile output before the JSON
10341034
from line 1039
10391039{"decision": "block", "reason": "Not allowed"}
10401040```
10411041
1042Unless the hook is [async](/docs/en/hooks#how-async-hooks-execute), Claude Code reads output that doesn't start with `{` as plain text, so your JSON is ignored. Because the hook exited 0, the transcript shows no error either. To check for this cause, start Claude Code with `claude --debug`, trigger the hook, and search the [debug log](/docs/en/hooks#debug-hooks) for `Hook output does not start with {`. To fix it, wrap the `echo` statements in your profile so they run only in interactive shells:
1042Unless the hook is [async](/docs/en/hooks.md#how-async-hooks-execute), Claude Code reads output that doesn't start with `{` as plain text, so your JSON is ignored. Because the hook exited 0, the transcript shows no error either. To check for this cause, start Claude Code with `claude --debug`, trigger the hook, and search the [debug log](/docs/en/hooks.md#debug-hooks) for `Hook output does not start with {`. To fix it, wrap the `echo` statements in your profile so they run only in interactive shells:
10431043
10441044```bash theme={null}
10451045# In ~/.zshrc or ~/.bashrc
from line 1052
10521052
10531053#### Fields at the wrong level
10541054
1055When your hook returns `permissionDecision` or `additionalContext` at the top level instead of inside `hookSpecificOutput`, the JSON still parses, and Claude Code ignores the misplaced fields without reporting an error. To see which fields it ignored, start Claude Code with `claude --debug` and search the [debug log](/docs/en/hooks#debug-hooks) for `Hook JSON output had unrecognized keys`.
1055When your hook returns `permissionDecision` or `additionalContext` at the top level instead of inside `hookSpecificOutput`, the JSON still parses, and Claude Code ignores the misplaced fields without reporting an error. To see which fields it ignored, start Claude Code with `claude --debug` and search the [debug log](/docs/en/hooks.md#debug-hooks) for `Hook JSON output had unrecognized keys`.
10561056
10571057### Check what a hook did
10581058
from line 1060
10601060
10611061* **Success**: you see nothing, unless the hook's JSON surfaces something, such as `systemMessage` or Stop hook feedback.
10621062 * To confirm the hook ran, check for its effect, like a reformatted file
1063* **Blocking error**: on most events you see the message that came with the block, for example `Blocked: rm commands are not allowed`. On a few events, such as `ConfigChange` and `Elicitation`, you see no message. [Exit code 2](/docs/en/hooks#exit-code-2) covers where the message comes from.
1063* **Blocking error**: on most events you see the message that came with the block, for example `Blocked: rm commands are not allowed`. On a few events, such as `ConfigChange` and `Elicitation`, you see no message. [Exit code 2](/docs/en/hooks.md#exit-code-2) covers where the message comes from.
10641064* **Non-blocking error**: you see a `<hook name> hook error` notice with a short explanation, such as the first line of stderr after `Failed with non-blocking status code:`, or a JSON validation or parse message. The action went ahead.
10651065
1066To look up the outcome for a specific exit code and stdout, including the per-event exceptions, see [Exit code output](/docs/en/hooks#exit-code-output) in the reference.
1066To look up the outcome for a specific exit code and stdout, including the per-event exceptions, see [Exit code output](/docs/en/hooks.md#exit-code-output) in the reference.
10671067
1068For full execution details, including hook exit codes, stdout, and stderr, read the [debug log](/docs/en/hooks#debug-hooks). Start Claude Code with `claude --debug-file /tmp/claude.log` to write to a known path, then `tail -f /tmp/claude.log` in another terminal. If you started without that flag, run `/debug` mid-session to enable logging and find the log path.
1068For full execution details, including hook exit codes, stdout, and stderr, read the [debug log](/docs/en/hooks.md#debug-hooks). Start Claude Code with `claude --debug-file /tmp/claude.log` to write to a known path, then `tail -f /tmp/claude.log` in another terminal. If you started without that flag, run `/debug` mid-session to enable logging and find the log path.
10691069
10701070## Learn more
10711071
1072* [Hooks reference](/docs/en/hooks): full event schemas, JSON output format, async hooks, and MCP tool hooks
1073* [Security considerations](/docs/en/hooks#security-considerations): review before deploying hooks in shared or production environments
1072* [Hooks reference](/docs/en/hooks.md): full event schemas, JSON output format, async hooks, and MCP tool hooks
1073* [Security considerations](/docs/en/hooks.md#security-considerations): review before deploying hooks in shared or production environments
10741074* [Bash command validator example](https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py): complete reference implementation
10751075
No line in this hunk matches that.