Claude Code Hooks: How They Work, With 8 Configs to Copy
Claude Code hooks are commands that Claude Code runs by itself at fixed points in a session: before a tool call, after a file edit, when Claude needs your input, when it finishes a reply. A hook fires every time its event happens, whether or not Claude remembers to. That makes it the place for anything that must happen without exception, like formatting each edited file or refusing every write to .env.
Below: where the config goes, every event, how a hook answers back, eight configs to paste in, and what to check when one stays silent. Everything here comes from Anthropic's Claude Code documentation as of October 2026.
Hook, CLAUDE.md or skill?
All three change how Claude Code behaves. What separates them is who decides whether they apply.
| Hook | CLAUDE.md | Skill | |
|---|---|---|---|
| What runs | A shell command, HTTP request, MCP tool call, prompt or subagent | Nothing: Claude reads it | Nothing: Claude reads and applies it |
| When | On a lifecycle event, like PostToolUse | Loaded at the start of every session | When you type /name or Claude matches its description |
| Who decides | Claude Code, every time the event fires | Claude, which tries to follow it | Claude, which interprets the steps |
| Context cost | Zero unless the hook returns output | The whole file, every request | The description each session, the full skill when used |
| Good for | Formatting, blocking unsafe commands, logging, notifications | Build commands, conventions, project facts | Workflows that need judgment, reference material |
Anthropic's own advice is to put guardrails in hooks. "Never edit .env" in CLAUDE.md is a request. A PreToolUse hook that blocks the edit is enforcement. For a procedure where Claude has to think through the steps, a skill fits better.
Where hooks are configured
Hooks live in a hooks block inside a Claude Code settings file. The file you pick sets who gets the hook:
| Location | Applies to | Shared? |
|---|---|---|
~/.claude/settings.json | All your projects | No, it stays on your machine |
.claude/settings.json | One project | Yes, commit it with the repo |
.claude/settings.local.json | One project | No, gitignored when Claude Code creates it |
| Managed policy settings | Your whole organization | Set by admins |
Plugin hooks/hooks.json | While the plugin is enabled | Yes, ships with the plugin |
| Skill frontmatter | The rest of the session once the skill runs | Yes, in the skill file |
| Subagent frontmatter | While that subagent runs | Yes, in the agent file |
Hooks from different files add up. If the exact same handler sits in two settings files, it runs once.
The config nests three levels deep: the event name, one or more matcher groups that filter when it fires, and the handlers that run. Every example below follows this shape:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "your-command-here" }
]
}
]
}
}If your settings file already has a hooks key, add the new event as another key inside it. Don't paste a second hooks object over the first.
Edits to settings files normally reach the running session on their own. Type /hooks to browse every configured hook and the file it came from, or describe the hook you want and let Claude write it.
Most hooks use "type": "command". The others are http (POST the event to a URL), mcp_tool, prompt (a Claude model makes the call) and the experimental agent.
Every hook event
The reference lists 33 events. The last column says what a hook can do about the thing that triggered it when it exits with code 2.
| Event | Fires when | Can block? |
|---|---|---|
SessionStart | A session begins or resumes | No |
Setup | You start with --init-only, or --init or --maintenance in -p mode | No |
UserPromptSubmit | You submit a prompt, before Claude processes it | Yes |
UserPromptExpansion | A typed command expands into a prompt | Yes |
PreToolUse | Before a tool call runs | Yes |
PermissionRequest | A tool call needs a permission decision | Through JSON only |
PermissionDenied | Auto mode denies a tool call | No |
PostToolUse | After a tool call succeeds | No, Claude sees your stderr |
PostToolUseFailure | After a tool call fails | No, Claude sees your stderr |
PostToolBatch | A batch of parallel tool calls resolves, before the next model call | Yes |
Notification | Claude Code sends a notification | No |
MessageDisplay | Assistant text is displayed | No |
SubagentStart | A subagent is spawned | No |
SubagentStop | A subagent finishes | Yes |
TaskCreated | A task is being created | Yes |
TaskCompleted | A task is being marked complete | Yes |
Stop | Claude finishes responding | Yes, Claude keeps working |
StopFailure | A turn ends on an API error | No |
TeammateIdle | An agent team teammate is about to go idle | Yes |
InstructionsLoaded | A CLAUDE.md or .claude/rules file loads | No |
ConfigChange | A configuration file changes mid-session | Yes, except policy settings |
CwdChanged | The working directory changes | No |
DirectoryAdded | A working directory is added mid-session | No |
FileChanged | A watched file changes on disk | No |
WorktreeCreate | A worktree is being created | Yes, any nonzero exit fails it |
WorktreeRemove | A worktree a hook created is being removed | Yes, any nonzero exit fails it |
PreCompact | Before context compaction | Yes |
PostCompact | After compaction completes | No |
PreModelSwitch | Before a model switch you or a client asked for | Yes |
PostModelSwitch | After the session's model changes | No |
Elicitation | An MCP server asks for your input during a tool call | Yes |
ElicitationResult | You answer an MCP elicitation, before the reply goes back | Yes |
SessionEnd | A session terminates | No |
Matchers: what a hook fires on
A matcher narrows an event. On tool events Claude Code tests it against the tool name; on others, against a value such as the notification type or how the session started. The characters in it decide how it's read:
"*",""or no matcher fires on every occurrence.- Only letters, digits,
_,-, spaces, commas and|means exact names.Bashmatches only Bash;Edit|WriteandEdit, Writematch either tool. - Any other character turns it into a JavaScript regular expression that matches anywhere in the value.
Edit.*also catchesNotebookEdit, so write^Edit$when you mean one tool.
Matchers are case-sensitive. MCP tools are named mcp__<server>__<tool>, so mcp__github__.* covers every tool from a server called github. Some events, including UserPromptSubmit, Stop and CwdChanged, take no matcher, and Claude Code silently ignores one if you add it.
On tool events, an if field on a handler filters by arguments with permission-rule syntax: "if": "Bash(git *)" runs the hook only for git commands. Anthropic calls this filter best-effort, so hard allow and deny lists belong in permission rules.
How a hook answers: exit codes and JSON
Claude Code hands the event to your command as JSON on stdin. Every event carries fields like session_id, cwd and hook_event_name. Tool events add tool_name and tool_input, which holds command for Bash and file_path for Edit and Write. Your hook replies through its exit code, stdout and stderr.
| Exit code | What happens |
|---|---|
| 0 | Success. Claude Code applies any JSON you printed. On SessionStart, UserPromptSubmit, UserPromptExpansion and PostModelSwitch, plain-text stdout goes into Claude's context |
| 2 | Blocking error on events that can block. Your stderr is the reason: Claude reads it after a blocked tool call, you see it after a blocked prompt |
| Anything else | Non-blocking error. The action goes ahead and the transcript shows a hook error notice with the first line of stderr |
The trap: exit 1 blocks nothing. A guard script that fails with exit 1 lets the tool call through. When you mean stop, exit 2.
For finer control, exit 0 and print one JSON object to stdout, with nothing else around it. Each event family reads different fields:
- PreToolUse returns
hookSpecificOutput.permissionDecision.allowskips the permission prompt,denycancels the call and sendspermissionDecisionReasonto Claude,askshows you the prompt, anddefer(with-ponly) pauses the call for later.updatedInputcan rewrite the tool's arguments first. - PostToolUse, Stop, UserPromptSubmit and several others take a top-level
"decision": "block"plus areason. - PermissionRequest answers with
hookSpecificOutput.decision.behavior, set toallowordeny. - Every event accepts
"continue": falsewith astopReason, which halts Claude completely, andsystemMessage, a warning shown to you.
To pass Claude information, return additionalContext inside hookSpecificOutput. At the top level, Claude Code drops it without an error. Write it as plain facts, like "This repo uses bun test": text that reads like a system command can trip Claude's prompt-injection defenses.
When several hooks match one event, they all run in parallel. For PreToolUse the strictest answer wins: deny, then defer, ask, allow.
Command hooks time out after 10 minutes by default (30 seconds on UserPromptSubmit). Set timeout in seconds on any handler to change it. SessionEnd hooks share a 1.5-second budget, so keep cleanup short.
8 hook configs you can copy
Each one is complete. Paste the JSON into the settings file named and merge it into your existing hooks block if there is one. The shell parts use jq to read the input, so install it first (brew install jq on macOS, apt-get install jq on Debian and Ubuntu).
1. Get a desktop notification when Claude needs you
The Notification event fires when Claude is waiting for permission or has gone idle after answering. Put this in ~/.claude/settings.json so it works in every project (macOS):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}On Linux, use notify-send 'Claude Code' 'Claude Code needs your attention' as the command. To be pinged only for approvals, set the matcher to permission_prompt.
If nothing pops up on a Mac, Script Editor probably lacks notification permission, and the command fails without a word. Run osascript -e 'display notification "test"' once, then allow Script Editor under System Settings > Notifications.
2. Format every file Claude edits
A PostToolUse hook on Edit|Write passes the edited path to Prettier. Add it to .claude/settings.json in the project root and swap in your own formatter command if you use another one:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}A successful run prints nothing in the chat, so check the file to confirm it ran. Blind spot: a file rewritten by a Bash command never passes through Edit or Write. For that, Anthropic points to a FileChanged hook.
3. Block edits to protected files
This is Anthropic's own example. Save the script as .claude/hooks/protect-files.sh:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Normalize Windows backslashes so the patterns match
FILE_PATH="${FILE_PATH//\\//}"
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 0Make it executable with chmod +x .claude/hooks/protect-files.sh, then register it in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}Ask Claude to add a comment to .env and it gets the "Blocked:" line as feedback and changes course. Skip the chmod on macOS or Linux and the hook simply never runs.
4. Stop dangerous shell commands, with a reason Claude can act on
This guard answers with JSON, so Claude gets a next step with the refusal. Save it as .claude/hooks/guard-bash.sh and chmod +x it:
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command // empty')
deny() {
jq -n --arg r "$1" '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $r}}'
exit 0
}
case "$COMMAND" in
*"rm -rf"*|*"rm -fr"*)
deny "Recursive force delete is blocked here. Delete specific files by path." ;;
*"git push --force"*|*"git push -f "*)
deny "Force push is blocked. Push to a new branch and open a pull request." ;;
*"drop table"*|*"DROP TABLE"*)
deny "Dropping tables is blocked. Write a migration." ;;
esac
exit 0Register it in .claude/settings.json. The args field switches the hook to exec form, which runs the script with no shell in between, so the path needs no quoting:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-bash.sh",
"args": []
}
]
}
]
}
}A hook's deny holds even in bypassPermissions mode or with --dangerously-skip-permissions. Text matching is easy to dodge, though (an extra space, a variable, a script file), so keep anything that must never run in your permission deny rules too.
5. Log every command Claude runs
Add this to ~/.claude/settings.json for a running log across all projects, one line per command with a timestamp and folder:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '\"\\(now | todate) \\(.cwd) \\(.tool_input.command)\"' >> ~/.claude/command-log.txt"
}
]
}
]
}
}PostToolUse fires only after a call succeeds. Failed commands arrive as PostToolUseFailure, so add the same matcher group under that event if you want those in the log as well.
6. Load fresh context at session start
Plain text a SessionStart hook prints goes straight into Claude's context. This one tells Claude the current branch and the last five commits:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Current branch: $(git branch --show-current)\"; echo 'Recent commits:'; git log --oneline -5"
}
]
}
]
}
}With no matcher it runs on every start, resume, /clear and compaction; set it to compact to run only after compaction. Rules that never change belong in CLAUDE.md. This hook is for facts that move, like the branch.
7. Make Claude run the tests before it stops
Exit code 2 on Stop keeps Claude working, and your stderr becomes its next instruction. Save this as .claude/hooks/require-tests.sh and make it executable:
#!/bin/bash
INPUT=$(cat)
# Claude is already continuing because of this hook: let it stop.
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
# Nothing changed since the last commit: skip the suite.
if git diff --quiet HEAD 2>/dev/null; then
exit 0
fi
if ! OUTPUT=$(npm test 2>&1); then
echo "Tests are failing. Fix them before you finish:" >&2
echo "$OUTPUT" | tail -n 40 >&2
exit 2
fi
exit 0{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/require-tests.sh",
"args": [],
"timeout": 300
}
]
}
]
}
}Replace npm test with your test command. Watch the loop: Stop fires after every reply, and Claude Code overrides a Stop hook that blocks eight times in a row, so the stop_hook_active check lets Claude hand back after one retry.
8. Approve the end of plan mode automatically
When a plan is ready, Claude calls ExitPlanMode and waits for your OK. This hook, in ~/.claude/settings.json, approves it for you:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}Keep that matcher exact. An empty matcher or .* here would approve every permission prompt, file writes and shell commands included.
When a hook doesn't fire
Work down this list in order.
- Check it loaded. Run
/hooksand find your hook under its event. If it's missing, look for invalid JSON (trailing commas and comments aren't allowed) or a file in the wrong folder. Restart the session if an edit hasn't shown up after a few seconds. - Compare the matcher. Matching is case-sensitive, so
bashwon't catchBash. Confirm the event too:PreToolUseruns before the tool,PostToolUseafter. - Run the script by hand. Pipe in sample JSON and print the exit code:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $? - Fix "command not found". Reference scripts by absolute path or through
${CLAUDE_PROJECT_DIR}, runchmod +x, and installjqif that's the name in the error. - JSON with no effect. Usually your shell profile echoes a line before the JSON, so stdout no longer starts with
{. Otherwise a field sits at the wrong level, likepermissionDecisionoutsidehookSpecificOutput. - Read the debug log. Start with
claude --debug(the log lands in~/.claude/debug/<session-id>.txt) orclaude --debug-file /tmp/claude.log, or run/debugmid-session. It records each hook's exit code, stdout and stderr. - Open the transcript view.
Ctrl+Oshows blocks and hook error notices. A successful hook leaves no trace there. - Ask about org policy. If
/hookssays "Only hooks from managed settings run here", an admin has setallowManagedHooksOnlyand your own hooks won't run.
To switch every hook off without deleting them, set "disableAllHooks": true in a settings file, or pass --settings '{"disableAllHooks": true}' for a single run. There's no switch for one hook at a time.
Security
Command hooks run with your full user permissions and can touch anything your account can, so review every hook before adding it.
- Workspace trust. Interactive sessions hold back every settings-file hook until you accept the folder's trust dialog. A
-por SDK run treats the folder as trusted, so a repo's committed hooks run at once. Before runningclaude -pover code you didn't write, read its.claude/folder, use--bare, or turn hooks off for that run. - Deny holds; allow has limits. A hook's deny works in every permission mode. A hook's allow can't override deny rules in your settings.
- Defensive scripts. Anthropic's checklist: validate and sanitize input, quote shell variables (
"$VAR"), reject paths containing.., use absolute paths, and stay away from.env,.git/and keys. - HTTP hooks. Headers resolve only the environment variables you list in
allowedEnvVars.
New to the tool? Start with how to use Claude Code, then browse our Claude Code prompts and the Claude skills directory.
Sources
- Anthropic, Hooks reference, Claude Code Docs, accessed October 2026
- Anthropic, Automate actions with hooks, Claude Code Docs, accessed October 2026
- Anthropic, Claude Code settings, accessed October 2026
- Anthropic, Extend Claude Code, accessed October 2026