Automate actions with hooks changedhooks-guide
Nearest release: v2.1.304, published 6 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 20 Oct 2026 02: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 20 Oct 2026 18:37 UTC.
Upstream edited
Recorded here
Lines+290added
Lines−298removed
From line
8
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits44to this page, all time
The whole hunk
from line 8, old and new numbered
/
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 8
88
99For 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.
1010
11<Tip>
12 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</Tip>
11> [!TIP]
12>
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).
1414
1515## Set up your first hook
1616
1717To create a hook, add a `hooks` block to a [settings file](#configure-hook-location). This walkthrough creates a desktop notification hook, so you get alerted whenever Claude is waiting for your input instead of watching the terminal.
1818
19<Steps>
20 <Step title="Add the hook to your settings">
21 Open `~/.claude/settings.json` and add a `Notification` hook. If the file doesn't exist, create it. The example below uses `osascript` for macOS; see [Get notified when Claude needs input](#get-notified-when-claude-needs-input) for Linux and Windows commands.
22
23 ```json theme={null}
24 {
25 "hooks": {
26 "Notification": [
27 {
28 "matcher": "",
29 "hooks": [
30 {
31 "type": "command",
32 "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
33 }
34 ]
35 }
36 ]
37 }
38 }
39 ```
40
41 If your settings file already has a `hooks` key, add `Notification` as a sibling of the existing event keys rather than replacing the whole object. Each event name is a key inside the single `hooks` object:
42
43 ```json theme={null}
44 {
45 "hooks": {
46 "PostToolUse": [
47 {
48 "matcher": "Edit|Write",
49 "hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
50 }
51 ],
52 "Notification": [
53 {
54 "matcher": "",
55 "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }]
56 }
57 ]
58 }
59 }
60 ```
61
62 You can also ask Claude to write the hook for you by describing what you want in the CLI.
63 </Step>
64
65 <Step title="Verify the configuration">
66 Type `/hooks` at the Claude Code prompt to open the hooks browser. Your new hook appears in the list under `Notification`.
67 </Step>
68
69 <Step title="Test the hook">
70 Press `Esc` to return to the CLI. Press `Shift+Tab` until the status bar shows `⏸ manual mode on`, ask Claude to do something that requires permission, then switch away from the terminal. You should receive a desktop notification.
71 </Step>
72</Steps>
19**1. Add the hook to your settings**
20
21Open `~/.claude/settings.json` and add a `Notification` hook. If the file doesn't exist, create it. The example below uses `osascript` for macOS; see [Get notified when Claude needs input](#get-notified-when-claude-needs-input) for Linux and Windows commands.
22
23```json theme={null}
24{
25 "hooks": {
26 "Notification": [
27 {
28 "matcher": "",
29 "hooks": [
30 {
31 "type": "command",
32 "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
33 }
34 ]
35 }
36 ]
37 }
38}
39```
40
41If your settings file already has a `hooks` key, add `Notification` as a sibling of the existing event keys rather than replacing the whole object. Each event name is a key inside the single `hooks` object:
42
43```json theme={null}
44{
45 "hooks": {
46 "PostToolUse": [
47 {
48 "matcher": "Edit|Write",
49 "hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }]
50 }
51 ],
52 "Notification": [
53 {
54 "matcher": "",
55 "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }]
56 }
57 ]
58 }
59}
60```
61
62You can also ask Claude to write the hook for you by describing what you want in the CLI.
63
64**2. Verify the configuration**
65
66Type `/hooks` at the Claude Code prompt to open the hooks browser. Your new hook appears in the list under `Notification`.
67
68**3. Test the hook**
69
70Press `Esc` to return to the CLI. Press `Shift+Tab` until the status bar shows `⏸ manual mode on`, ask Claude to do something that requires permission, then switch away from the terminal. You should receive a desktop notification.
7371
7472## What you can automate
7573
from line 85
8785
8886Each tab below uses the platform's native notification command. Add this to `~/.claude/settings.json`:
8987
90<Tabs>
91 <Tab title="macOS">
92 ```json theme={null}
93 {
94 "hooks": {
95 "Notification": [
96 {
97 "matcher": "",
98 "hooks": [
99 {
100 "type": "command",
101 "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
102 }
103 ]
104 }
105 ]
106 }
107 }
108 ```
109
110 <Accordion title="If no notification appears">
111 `osascript` routes notifications through the built-in Script Editor app. If Script Editor doesn't have notification permission, the command fails silently, and macOS won't prompt you to grant it.
112
113 Run this in Terminal once to make Script Editor appear in your notification settings:
114
115 ```bash theme={null}
116 osascript -e 'display notification "test"'
117 ```
118
119 Nothing will appear yet. Open **System Settings > Notifications**, find **Script Editor** in the list, and turn on **Allow Notifications**. Run the command again to confirm the test notification appears.
120 </Accordion>
121 </Tab>
122
123 <Tab title="Linux">
124 ```json theme={null}
125 {
126 "hooks": {
127 "Notification": [
128 {
129 "matcher": "",
130 "hooks": [
131 {
132 "type": "command",
133 "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
134 }
135 ]
136 }
137 ]
138 }
139 }
140 ```
141
142 <Accordion title="If no notification appears">
143 `notify-send` needs a desktop notification daemon, which headless servers, SSH sessions, and most containers don't have. Test the command directly first:
144
145 ```bash theme={null}
146 notify-send 'Claude Code' 'test'
147 ```
148
149 If the command isn't found, install the `libnotify-bin` package on Debian and Ubuntu, or your distribution's equivalent.
150 </Accordion>
151 </Tab>
152
153 <Tab title="Windows (PowerShell)">
154 ```json theme={null}
155 {
156 "hooks": {
157 "Notification": [
158 {
159 "matcher": "",
160 "hooks": [
161 {
162 "type": "command",
163 "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
164 }
165 ]
166 }
167 ]
168 }
169 }
170 ```
171
172 <Accordion title="If no dialog appears">
173 This command opens a dialog box rather than a notification in the corner of your screen, so the dialog can open behind your terminal window. Test the command directly in PowerShell first.
174
175 If you run Claude Code inside WSL, `powershell.exe` must be available on your `PATH` through Windows interop.
176 </Accordion>
177 </Tab>
178</Tabs>
88**macOS**
89
90```json theme={null}
91{
92 "hooks": {
93 "Notification": [
94 {
95 "matcher": "",
96 "hooks": [
97 {
98 "type": "command",
99 "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
100 }
101 ]
102 }
103 ]
104 }
105}
106```
107
108> **If no notification appears**
109>
110> `osascript` routes notifications through the built-in Script Editor app. If Script Editor doesn't have notification permission, the command fails silently, and macOS won't prompt you to grant it.
111>
112> Run this in Terminal once to make Script Editor appear in your notification settings:
113>
114> ```bash theme={null}
115> osascript -e 'display notification "test"'
116> ```
117>
118> Nothing will appear yet. Open **System Settings > Notifications**, find **Script Editor** in the list, and turn on **Allow Notifications**. Run the command again to confirm the test notification appears.
119
120**Linux**
121
122```json theme={null}
123{
124 "hooks": {
125 "Notification": [
126 {
127 "matcher": "",
128 "hooks": [
129 {
130 "type": "command",
131 "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
132 }
133 ]
134 }
135 ]
136 }
137}
138```
139
140> **If no notification appears**
141>
142> `notify-send` needs a desktop notification daemon, which headless servers, SSH sessions, and most containers don't have. Test the command directly first:
143>
144> ```bash theme={null}
145> notify-send 'Claude Code' 'test'
146> ```
147>
148> If the command isn't found, install the `libnotify-bin` package on Debian and Ubuntu, or your distribution's equivalent.
149
150**Windows (PowerShell)**
151
152```json theme={null}
153{
154 "hooks": {
155 "Notification": [
156 {
157 "matcher": "",
158 "hooks": [
159 {
160 "type": "command",
161 "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
162 }
163 ]
164 }
165 ]
166 }
167}
168```
169
170> **If no dialog appears**
171>
172> This command opens a dialog box rather than a notification in the corner of your screen, so the dialog can open behind your terminal window. Test the command directly in PowerShell first.
173>
174> If you run Claude Code inside WSL, `powershell.exe` must be available on your `PATH` through Windows interop.
179175
180176The empty `matcher` fires on all notification types. To fire only on specific events, set it to one of these values:
181177
from line 230
234230
235231To reformat a specific file however it changes, including when a `Bash` command rewrites it, use a [FileChanged](/docs/en/hooks#filechanged) hook instead.
236232
237<Note>
238 The Bash examples on this page use `jq` for JSON parsing. Install it with `brew install jq` on macOS, `apt-get install jq` on Debian and Ubuntu, or see [`jq` downloads](https://jqlang.org/download/).
239</Note>
233> [!NOTE]
234>
235> The Bash examples on this page use `jq` for JSON parsing. Install it with `brew install jq` on macOS, `apt-get install jq` on Debian and Ubuntu, or see [`jq` downloads](https://jqlang.org/download/).
240236
241237### Block edits to protected files
242238
from line 240
244240
245241This example uses a separate script file that the hook calls. The script checks the target file path against a list of protected patterns and exits with code 2 to block the edit.
246242
247<Steps>
248 <Step title="Create the hook script">
249 Save this to `.claude/hooks/protect-files.sh`:
250
251 ```bash theme={null}
252 #!/bin/bash
253 # protect-files.sh
254
255 INPUT=$(cat)
256 FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
257
258 # Normalize Windows backslash separators so the patterns below match
259 FILE_PATH="${FILE_PATH//\\//}"
260
261 PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
262
263 for pattern in "${PROTECTED_PATTERNS[@]}"; do
264 if [[ "$FILE_PATH" == *"$pattern"* ]]; then
265 echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
266 exit 2
267 fi
268 done
269
270 exit 0
271 ```
272 </Step>
273
274 <Step title="Make the script executable on macOS and Linux">
275 Hook scripts must be executable for Claude Code to run them:
276
277 ```bash theme={null}
278 chmod +x .claude/hooks/protect-files.sh
279 ```
280 </Step>
281
282 <Step title="Register the hook">
283 Add a `PreToolUse` hook to `.claude/settings.json` that runs the script before any `Edit` or `Write` tool call:
284
285 ```json theme={null}
286 {
287 "hooks": {
288 "PreToolUse": [
289 {
290 "matcher": "Edit|Write",
291 "hooks": [
292 {
293 "type": "command",
294 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
295 }
296 ]
297 }
298 ]
299 }
300 }
301 ```
302 </Step>
303
304 <Step title="Test the hook">
305 Ask Claude to add a comment to your `.env` file. Claude Code blocks the edit before it runs and passes the script's `Blocked:` message to Claude as feedback.
306 </Step>
307</Steps>
243**1. Create the hook script**
244
245Save this to `.claude/hooks/protect-files.sh`:
246
247```bash theme={null}
248#!/bin/bash
249# protect-files.sh
250
251INPUT=$(cat)
252FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
253
254# Normalize Windows backslash separators so the patterns below match
255FILE_PATH="${FILE_PATH//\\//}"
256
257PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
258
259for pattern in "${PROTECTED_PATTERNS[@]}"; do
260 if [[ "$FILE_PATH" == *"$pattern"* ]]; then
261 echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
262 exit 2
263 fi
264done
265
266exit 0
267```
268
269**2. Make the script executable on macOS and Linux**
270
271Hook scripts must be executable for Claude Code to run them:
272
273```bash theme={null}
274chmod +x .claude/hooks/protect-files.sh
275```
276
277**3. Register the hook**
278
279Add a `PreToolUse` hook to `.claude/settings.json` that runs the script before any `Edit` or `Write` tool call:
280
281```json theme={null}
282{
283 "hooks": {
284 "PreToolUse": [
285 {
286 "matcher": "Edit|Write",
287 "hooks": [
288 {
289 "type": "command",
290 "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
291 }
292 ]
293 }
294 ]
295 }
296}
297```
298
299**4. Test the hook**
300
301Ask Claude to add a comment to your `.env` file. Claude Code blocks the edit before it runs and passes the script's `Blocked:` message to Claude as feedback.
308302
309303### Re-inject context after compaction
310304
from line 441
447441
448442To set a specific permission mode instead, your hook's output can include an `updatedPermissions` array with a `setMode` entry. The `mode` value is any permission mode like `default`, `acceptEdits`, or `bypassPermissions`, and `destination: "session"` applies it for the current session only.
449443
450<Note>
451 `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).
452
453 Claude Code never saves it as `defaultMode`.
454</Note>
444> [!NOTE]
445>
446> `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>
448> Claude Code never saves it as `defaultMode`.
455449
456450To switch the session to `acceptEdits`, your hook writes this JSON to stdout:
457451
from line 602
608602
609603Exit codes only let you block or stay silent. For more control, exit 0 and print a JSON object to stdout instead.
610604
611<Note>
612 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).
613</Note>
605> [!NOTE]
606>
607> 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).
614608
615609For example, a `PreToolUse` hook can deny a tool call and tell Claude why, or escalate it to the user for approval:
616610
from line 670
676670
677671The `"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.
678672
679<Note>
680 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.
681</Note>
673> [!NOTE]
674>
675> 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.
682676
683677Each event type matches on a specific field:
684678
from line 699
705699
706700The tabs below show a few more matchers on different event types.
707701
708<Tabs>
709 <Tab title="Log every Bash command">
710 Match only `Bash` tool calls and log each command to a file. The `PostToolUse` event fires after the command completes, so `tool_input.command` contains what ran. The hook receives the event data as JSON on stdin, and `jq -r '.tool_input.command'` extracts only the command string, which `>>` appends to the log file:
711
712 ```json theme={null}
713 {
714 "hooks": {
715 "PostToolUse": [
716 {
717 "matcher": "Bash",
718 "hooks": [
719 {
720 "type": "command",
721 "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
722 }
723 ]
724 }
725 ]
726 }
727 }
728 ```
729 </Tab>
730
731 <Tab title="Match MCP tools">
732 MCP 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.
733
734 The 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):
735
736 ```json theme={null}
737 {
738 "hooks": {
739 "PreToolUse": [
740 {
741 "matcher": "mcp__github__.*",
742 "hooks": [
743 {
744 "type": "command",
745 "command": "echo \"GitHub tool called: $(jq -r '.tool_name')\" >&2"
746 }
747 ]
748 }
749 ]
750 }
751 }
752 ```
753 </Tab>
754
755 <Tab title="Clean up on session end">
756 The `SessionEnd` event supports matchers on the reason the session ended. This hook only fires on the `clear` reason, set when you run `/clear`, not on normal exits:
757
758 ```json theme={null}
759 {
760 "hooks": {
761 "SessionEnd": [
762 {
763 "matcher": "clear",
764 "hooks": [
765 {
766 "type": "command",
767 "command": "rm -f /tmp/claude-scratch-*.txt"
768 }
769 ]
770 }
771 ]
772 }
773 }
774 ```
775 </Tab>
776</Tabs>
702**Log every Bash command**
703
704Match only `Bash` tool calls and log each command to a file. The `PostToolUse` event fires after the command completes, so `tool_input.command` contains what ran. The hook receives the event data as JSON on stdin, and `jq -r '.tool_input.command'` extracts only the command string, which `>>` appends to the log file:
705
706```json theme={null}
707{
708 "hooks": {
709 "PostToolUse": [
710 {
711 "matcher": "Bash",
712 "hooks": [
713 {
714 "type": "command",
715 "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
716 }
717 ]
718 }
719 ]
720 }
721}
722```
723
724**Match MCP tools**
725
726MCP 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.
727
728The 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):
729
730```json theme={null}
731{
732 "hooks": {
733 "PreToolUse": [
734 {
735 "matcher": "mcp__github__.*",
736 "hooks": [
737 {
738 "type": "command",
739 "command": "echo \"GitHub tool called: $(jq -r '.tool_name')\" >&2"
740 }
741 ]
742 }
743 ]
744 }
745}
746```
747
748**Clean up on session end**
749
750The `SessionEnd` event supports matchers on the reason the session ended. This hook only fires on the `clear` reason, set when you run `/clear`, not on normal exits:
751
752```json theme={null}
753{
754 "hooks": {
755 "SessionEnd": [
756 {
757 "matcher": "clear",
758 "hooks": [
759 {
760 "type": "command",
761 "command": "rm -f /tmp/claude-scratch-*.txt"
762 }
763 ]
764 }
765 ]
766 }
767}
768```
777769
778770#### Filter by tool name and arguments with the `if` field
779771
from line 864
872864
873865## Agent-based hooks
874866
875<Warning>
876 Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](/docs/en/hooks#command-hook-fields).
877</Warning>
867> [!WARNING]
868>
869> Agent hooks are experimental. Behavior and configuration may change in future releases. For production workflows, prefer [command hooks](/docs/en/hooks#command-hook-fields).
878870
879871When 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.
880872
from line 964
972964 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.
973965* **`"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.
974966
975<Note>
976 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.
977
978 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).
979</Note>
967> [!NOTE]
968>
969> 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>
971> 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).
980972
981973### Hook not firing
982974
983975
No line in this hunk matches that.