Claude Code hooks examples: format, block, notify
Copy-paste Claude Code hooks examples checked against the official docs: format after edits, block risky commands with exit code 2, and get notified.
Claude Code hooks run your own commands at fixed points in Claude’s lifecycle, so formatting, guardrails and notifications happen every time instead of when Claude remembers. These Claude Code hooks examples come from the hooks guide and the hooks reference, checked against both pages on 2026-10-10. Event names and fields change between releases, so confirm against the current docs.
What hooks are and when to use them instead of CLAUDE.md or a skill
A hook is a handler (usually a shell command) that Claude Code runs when an event fires, such as PreToolUse (before a tool runs), PostToolUse (after) or Notification. The key difference from instructions is that a hook is deterministic: Claude Code runs it, the model does not choose to.
- Use a hook when something must happen every time: format, block, log, notify.
- Use CLAUDE.md for conventions Claude should follow in every session.
- Use a Claude Code skill for task-specific instructions that should load only when relevant.
Where hooks are configured (settings files and scope)
Hooks live in a hooks block inside a settings file. Per the hooks guide:
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json |
All your projects | No, local to your machine |
.claude/settings.json |
Single project | Yes, can be committed |
.claude/settings.local.json |
Single project | No |
Plugins (hooks/hooks.json) and skill frontmatter can also define hooks. Each event name is a key inside the single hooks object. A matcher narrows which tool or notification type triggers the group; without one, the hook fires on every occurrence of the event. Matchers are case-sensitive. Run /hooks to browse what is configured.
Example 1: format files after every edit (PostToolUse)
From the guide: run Prettier on every file Claude edits. Add to .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
The hook receives the event as JSON on stdin; jq extracts the edited path and passes it to Prettier. It needs jq installed. To test it, ask Claude to add a line with single-quoted strings to a JavaScript file: with default Prettier settings they become double quotes.
Limit: the Edit|Write matcher does not see files a Bash command rewrites. The guide points to a FileChanged hook for that case.
Example 2: block a dangerous command (PreToolUse and exit code 2)
Exit code 2 is the blocking signal. The guide’s protected-files example shows the pattern for Edit|Write; the script below applies the same pattern to Bash commands. The tool_input.command field is the Bash tool’s input; this script is our adaptation, not an official sample, so test it before relying on it.
Save as .claude/hooks/block-dangerous.sh:
#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -Eq 'rm -rf|DROP TABLE'; then
echo "Blocked: '$CMD' matches a dangerous pattern" >&2 # stderr becomes Claude's feedback
exit 2 # exit 2 = block the action
fi
exit 0 # no objection; the normal permission flow still applies
Make it executable with chmod +x .claude/hooks/block-dangerous.sh, then register it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}
Exit-code behaviour, per the reference:
- Exit 2 on
PreToolUseblocks the tool call, and the stderr message goes back to Claude so it can adjust. - Exit 0 is not approval. The normal permission flow still applies.
- Other non-zero codes are non-blocking errors: the action proceeds and the transcript shows a hook error.
A string match is a guardrail, not a security boundary: the guide itself says to use the permission system rather than a hook to enforce a hard allow or deny. Pick one output style per hook, exit codes or JSON, not both.
Example 3: get notified when Claude needs input
The Notification event fires when Claude Code is waiting for input or permission. The guide’s Linux version:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
The guide gives osascript (macOS) and PowerShell (Windows) variants too. An empty matcher fires on all notification types. To narrow it, use permission_prompt (an approval prompt that has waited about six seconds) or idle_prompt (Claude finished about 60 seconds ago and you have not typed). notify-send needs a notification daemon, which headless servers and most containers lack, so test the command directly first.
Debug a hook that does not fire
Following the guide’s troubleshooting section:
- Run
/hooksand confirm the hook is listed under the right event. - Check the matcher against the tool name exactly; matchers are case-sensitive.
- Confirm the event type:
PreToolUsefires before the tool,PostToolUseafter. - Validate your JSON: trailing commas and comments are not allowed.
- Test the script by hand:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?
- Make scripts executable and reference them via
$CLAUDE_PROJECT_DIR. Ifjqis missing, install it or parse JSON with Python or Node.
For full stderr, use claude --debug or /debug mid-session. If /hooks shows “Only hooks from managed settings run here”, your organization has restricted hooks and yours will not run.
What to change in your setup
- Add the Prettier hook to the project’s
.claude/settings.jsonso the whole team gets it, and installjqin CI and dev containers. - Put guardrail scripts in
.claude/hooks/, commit them, and keep matchers narrow (Bash,Edit|Write). - Treat hook blocks as feedback to Claude, and keep real enforcement in permission rules.
- Put the notification hook in
~/.claude/settings.json, since it is personal, and test the command outside Claude Code first. - Test every hook by piping sample JSON before trusting it.
FAQ
- What exit code blocks an action in a Claude Code hook?
- Exit code 2. Write the reason to stderr. On PreToolUse the tool call is blocked and the reason is fed back to Claude. Exit 0 means no objection, and for PreToolUse it does not approve the call; the normal permission flow still applies.
- Where do I put Claude Code hooks?
- In a settings file. ~/.claude/settings.json applies to all your projects, .claude/settings.json applies to one project and can be committed, and .claude/settings.local.json is a per-project file that is not shared.
- Does a PostToolUse formatter see files changed by Bash commands?
- No. A matcher of Edit|Write only fires for those two tools. The docs suggest a FileChanged hook to reformat a file however it changes.