Last updated: November 11, 2026
TL;DR
Claude Code hooks are shell commands that Claude Code runs at fixed points in a session, so your formatter or guardrail runs every time instead of whenever the model remembers. You add them to a settings file under an event name like PreToolUse. A hook blocks an action by exiting with code 2. Exit code 1 blocks nothing.
- ๐จ Format on edit. A
PostToolUsehook onEdit|Writeruns your formatter after every change. - ๐ Block a file. A
PreToolUsehook checks the path and exits 2, and Claude gets your message as feedback. - ๐ Fail closed. Add
"onFailure": "block", because a guardrail that crashes lets the action through by default. - โ
Finish on green. A
Stophook sends Claude back to work when the tests fail. - ๐ Check it.
/hookslists what's configured, andCtrl+Oshows what each hook did.
Anthropic's docs list 33 hook events as of Claude Code 2.1.319. By our count of Anthropic's exit code table on November 9, 2026, exit code 2 can stop the action on 16 of them. On the other 17 it can't, including PostToolUse, where the tool has already run.
Before you start
Hooks need nothing beyond Claude Code and a settings file. Four things shape how these steps go:
jqinstalled. The commands below parse the hook's JSON input with it. Runbrew install jqon macOS orapt-get install jqon Debian and Ubuntu.- Claude Code 2.1.295 or later for Step 3. That's when
onFailurearrived. Runclaude updateif you're behind. - A trusted folder. In an interactive session, Claude Code holds back hooks from every settings file until you accept the workspace trust dialog for that folder.
- A file for the hooks. These steps use
.claude/settings.jsonin your project, which you can commit so your team runs the same hooks. If the file already has ahookskey, add each event beside the ones already there.
Step 1: Format every file Claude edits
Add this to .claude/settings.json. Edit|Write means the hook runs only after those two tools, and -w is Prettier's short flag for writing the file in place:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier -w"
}
]
}
]
}
}
To test it, ask Claude to add a line with single-quoted strings to a JavaScript file. With Prettier's defaults, the quotes become double quotes. A hook that succeeds shows nothing in the conversation, so the reformatted file is your confirmation.
Step 2: Block edits to protected files
A PreToolUse hook runs before the tool does, so this is the event that can stop a change. Save this script as .claude/hooks/protect-files.sh:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
Make it executable:
chmod +x .claude/hooks/protect-files.sh
Then test the script on its own, with no session involved. It should print the Blocked: line and then 2:
echo '{"tool_name":"Edit","tool_input":{"file_path":".env"}}' | .claude/hooks/protect-files.sh; echo $?
Register it next to the formatter, as a second key inside hooks:
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-files.sh",
"args": []
}
]
}
]
The empty args array makes Claude Code start the script directly, with no shell in between, so there's no quoting to get wrong. Ask Claude to add a comment to your .env file. Claude Code stops the edit before it runs, and Claude receives the Blocked: message as feedback and can change course.
Step 3: Make the guardrail fail closed
Add "onFailure": "block" to any hook that enforces a rule. On most events, a hook that fails or times out lets the action go ahead, so a wrong path or a crashing script turns your guardrail into nothing at all, with only a notice in the transcript to tell you. Change the handler from Step 2 to this:
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-files.sh",
"args": [],
"onFailure": "block"
}
With that set, Claude Code treats each of these as a block: the script can't start, it exits with anything other than 0 or 2, it reaches its timeout, or it prints JSON that can't be parsed.
To test it, rename the script and ask Claude to edit any file. The edit is blocked, and the error includes failed; blocking because onFailure is "block". Rename the script back afterward.
Step 4: Send Claude back when the tests fail
A Stop hook runs when Claude finishes responding, and exit code 2 keeps it working. Save this as .claude/hooks/tests-pass.sh, swap in your own test command and run chmod +x on it:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! npm test >/dev/null 2>&1; then
echo "Tests are failing. Fix them before you finish." >&2
exit 2
fi
exit 0
Register it under Stop, with no matcher, because Stop doesn't use one:
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/tests-pass.sh",
"args": []
}
]
}
]
The stop_hook_active check gives Claude one extra pass and then lets it stop, even if the tests are still red. Without that check, Claude Code overrides the hook itself after eight blocks in a row with no tool call in between. Stop fires every time Claude finishes responding, so a slow test suite runs after a one-line answer too.
Step 5: Confirm what's running
Type /hooks in Claude Code. It opens a read-only list of every configured hook, labeled by where it came from. Then press Ctrl+O for the transcript view, where a hook's outcome is one of these three:
| What you see | What it means |
|---|---|
| Nothing | The hook succeeded. Check its effect, such as a reformatted file |
Your Blocked: message | The hook exited 2 and stopped the action |
A <hook name> hook error notice | A non-blocking error. The action went ahead |
The third row is the one to look for on the first run of a new guardrail. It means your hook failed and the action went through anyway.
Where Claude Code hooks stop protecting you
A hook guards only the calls it matches, so treat it as one layer. Anthropic's docs say to use the permission system rather than a hook to enforce a hard allow or deny. Here are the gaps that matter most:
- The matcher sees only the tools you name. The Step 2 hook watches
EditandWrite. ABashcommand that writes to.envdoesn't match it. - The
iffilter is best-effort. It narrows a hook by arguments, such asBash(git *), and Anthropic's docs describe it in those words. - A deny doesn't stop other hooks. Every matching hook runs to completion, in parallel, before Claude Code merges the results. A logging hook still logs a command that another hook denied.
PostToolUsecan't undo anything. The tool has already run, so exit 2 there only shows your message to Claude.- A hook's allow can't beat a settings deny rule. A hook returning
"allow"doesn't bypass deny rules from settings.
One strength is worth knowing. A PreToolUse hook that returns a deny holds in every permission mode, including bypass mode, because those hooks fire before any permission-mode check.
Hooks also carry their own risk. Command hooks run with your full user permissions. In a non-interactive claude -p run, Claude Code never shows the trust dialog, so hooks committed to a repository's .claude/settings.json run in a folder you've never trusted. Read a repo's .claude/ files before you script Claude Code over it.
Reference: which events can stop the action
Exit code 2 stops the action on these 16 events, and it does something different on each:
| Event | What exit 2 does |
|---|---|
PreToolUse | Blocks the tool call |
PostToolBatch | Stops the agentic loop before the next model call |
UserPromptSubmit | Blocks the prompt, so it never reaches Claude |
UserPromptExpansion | Blocks the expansion |
Stop | Keeps Claude working |
SubagentStop | Keeps the subagent working |
TeammateIdle | Keeps the agent team teammate working |
TaskCreated | Rolls back the task creation |
TaskCompleted | Stops the task being marked complete |
ConfigChange | Blocks the config change, except policy_settings |
PreCompact | Blocks compaction |
PreModelSwitch | Blocks the model switch |
Elicitation | Declines the MCP server's request |
ElicitationResult | Blocks the response |
WorktreeCreate | Any non-zero exit fails the creation |
WorktreeRemove | Any non-zero exit fails the removal if the directory is still there |
PermissionRequest looks like it belongs on this list, and it doesn't. Exit 2 isn't honored there, so deny through the JSON decision object instead.
Reference: exit codes
| Exit code | Effect |
|---|---|
| 0 | Success. For PreToolUse this is no decision, and the normal permission flow still applies |
| 2 | Blocks the action on the events above. Stderr is the message |
| Anything else | Non-blocking error. The action goes ahead, unless onFailure is "block" |
To send a structured decision instead, exit 0 and print JSON. For PreToolUse, permissionDecision goes inside hookSpecificOutput. At the top level, Claude Code ignores it and reports no error.
Reference: where hooks live
| Location | Scope | Shared with your team |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | One project | Yes, if committed |
.claude/settings.local.json | One project | No |
| Managed policy settings | Organization-wide | Yes, set by an admin |
Plugin hooks/hooks.json | While the plugin is enabled | Yes |
| Skill frontmatter | Rest of the session after the skill runs | Yes |
| Subagent frontmatter | While that subagent runs | Yes |
Hook entries merge across these levels rather than replacing each other.
Reference: hook types and timeouts
| Type | What it does | Default timeout |
|---|---|---|
command | Runs a shell command | 10 minutes |
http | POSTs the event JSON to a URL | 10 minutes |
mcp_tool | Calls a tool on a configured MCP server | 10 minutes |
prompt | Asks a Claude model for a single-turn verdict | 30 seconds |
agent | Runs a subagent that can use tools (experimental) | 60 seconds |
The default for command, http and mcp_tool drops to 30 seconds on UserPromptSubmit, PreModelSwitch and PostModelSwitch, and to 10 seconds on MessageDisplay. Set timeout in seconds on any hook to override it.
Troubleshooting
| Symptom | Check |
|---|---|
| The hook never runs | Run /hooks and confirm it's under the right event. Matchers are case-sensitive, so edit won't match Edit |
/hooks shows nothing | Look for a trailing comma or comment in the JSON, and confirm the file's location. Restart the session if a file edit wasn't picked up |
| "command not found" | Use ${CLAUDE_PROJECT_DIR} or an absolute path |
| The script doesn't run at all | Run chmod +x on it |
| The hook runs but JSON does nothing | A shell profile echo may be printing first, or a field may be at the wrong level. Run /debug and read the log |
| "Only hooks from managed settings run here" | Your organization set allowManagedHooksOnly, so your own hooks don't run |
Anthropic's hooks guide has the full troubleshooting list, and the hooks reference has every event's input and output.
Frequently asked questions
What are Claude Code hooks?
Claude Code hooks are user-defined shell commands that run at fixed points in a session, such as before a tool call or when Claude finishes responding. They run every time their event fires, so a rule doesn't depend on the model remembering it.
Where do I put Claude Code hooks?
Put them under a hooks key in a settings file. .claude/settings.json applies to one project and can be committed for your team, and ~/.claude/settings.json applies to all your projects. Plugins, skills and subagents can also define hooks.
How do I block a tool call with a hook?
Register a PreToolUse hook on the tool, write the reason to stderr, and exit with code 2. Claude Code stops the call and passes your message to Claude. You can also exit 0 and print JSON with a permissionDecision of deny.
Why doesn't exit code 1 block the action?
Claude Code treats any exit code other than 0 or 2 as a non-blocking error, so the action goes ahead and a notice appears in the transcript. Use exit code 2 to block, or set "onFailure": "block" so that a failing hook blocks as well.
Can a hook override a permission deny rule?
No. A hook returning "allow" doesn't bypass deny rules from settings. The reverse does hold: a PreToolUse hook that returns a deny blocks the tool in every permission mode, including bypass mode.
Do Claude Code hooks run inside subagents?
Yes. Hooks from settings files, managed policy settings and plugins also run inside subagents. When a subagent calls a tool, tool events such as PreToolUse and PostToolUse fire the same configured hooks as in the main conversation.
How do I turn off all hooks?
Set "disableAllHooks": true in your settings file. Claude Code reads the value left after settings precedence, so a project's file can override yours. Hooks in managed settings keep running unless disableAllHooks is also set there.
What's the difference between hooks and CLAUDE.md?
CLAUDE.md is written guidance that Claude reads and applies with judgment. A hook is a command that Claude Code runs itself when its event fires, so use CLAUDE.md for conventions and hooks for anything that has to happen every time.
Pair the hook with the rule behind it
A hook enforces one rule at one point in the session. The reasoning behind that rule still has to live somewhere Claude reads, or it will keep trying the blocked edit and working around the block. Our guide to compound engineering for Claude Code shows how to build those written rules up one fix at a time, and our subagents guide covers giving a narrow job its own context and tools.
Stop guessing. Score your first workflow.
Score one workflow in about five minutes. Nine questions, a grade, and the specific gap holding it back - no call required.
Related Posts

Claude Sheets, Whiteboards and Data: the admin defaults to check
Anthropic launched Claude Sheets and Whiteboards in beta on November 10, 2026, and renamed Claude Dashboards to Claude Data. What's already on for Team plans, what Enterprise turns on November 19, why live sharing is the setting to read first, and where to change it.

Claude Code best practices for a team: what to write down and what to enforce
Claude Code best practices for a team: what belongs in CLAUDE.md, which rules to enforce with hooks, deny rules and managed settings, how to scope them, and how to measure a pilot.

Claude Fable 5.2 in Claude Code: what it costs and when to switch
Anthropic released Claude Fable 5.2 on November 9, 2026, and Claude Code needs 2.1.321 to run it. What it costs next to Opus 5.5 and Fable 5.1, what the update changes about /model fable and /model best, and why switching mid-session re-reads the whole conversation at full price.
