
Claude Code Hooks: Auto-Format, Block Secrets, and Notify — Wired in settings.json
Chris Harper
3 min read
Jun 30, 2026 · 20:13 UTC
TL;DR: Claude Code hooks run any shell command before or after every tool call — wire them in .claude/settings.json to auto-format code, block unsafe operations, or send a notification when a multi-hour task completes.
Hooks are configured in .claude/settings.json (project-level, git-committed, shared by the team) or ~/.claude/settings.json (user-level, all projects). Each entry names an event, an optional tool matcher, and the command to run:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/secret-scan.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/format.sh" }
]
}
]
}
}
The four hook events
| Event | Fires | Can block? |
|---|---|---|
PreToolUse | Before any tool call | Yes — exit 2 blocks the tool |
PostToolUse | After a tool succeeds | No |
PostToolUseFailure | After a tool errors | No |
Stop | When Claude finishes a turn | No |
Hook input and exit codes
Every hook receives the tool's full JSON payload on stdin (read it with cat):
input="$(cat)"
# Extract the Bash command being run
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
# For Edit/Write hooks, use .tool_input.file_path instead
Exit codes:
0→ proceed normally2→ block the tool; your stderr becomes Claude's feedback so it can adjust- anything else → proceed, error is logged
Pattern 1 — Block commits containing secrets
#!/usr/bin/env bash
set -euo pipefail
input="$(cat)"
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')
case "$cmd" in
*"git commit"*|*"git push"*)
diff="$(git diff --cached 2>/dev/null || true)"
if printf '%s' "$diff" \
| grep -Eq 'sk-ant-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}'; then
echo "Blocked: secret detected in staged diff." >&2
exit 2
fi
;;
esac
exit 0
Wire as a PreToolUse hook with "matcher": "Bash". When Claude tries to commit, the hook checks the diff; if a key is found, it exits 2 — Claude sees the block reason and will offer to remove the secret before retrying.
Pattern 2 — Auto-format after every file edit
#!/usr/bin/env bash
set -euo pipefail
fp=$(cat | jq -r '.tool_input.file_path // empty')
[ -z "$fp" ] && exit 0
case "$fp" in
*.py) black "$fp" 2>/dev/null ;;
*.ts|*.tsx|*.js) prettier --write "$fp" 2>/dev/null ;;
*.go) gofmt -w "$fp" ;;
esac
exit 0
Wire as PostToolUse with "matcher": "Write|Edit|MultiEdit". Claude's next read of that file sees the formatted version — no separate "please format" prompt needed.
Pattern 3 — Desktop notification when a long task finishes
#!/usr/bin/env bash
# macOS / Linux
osascript -e 'display notification "Task complete" with title "Claude Code"' \
2>/dev/null \
|| notify-send "Claude Code" "Task complete" 2>/dev/null
exit 0
Wire as a Stop hook (no matcher — Stop has no tool to filter). After every session turn, the notification fires. Set timeout: 0 if the notification command might exceed the default 10-second limit.
Matchers
The matcher field is a regex matched against the tool name:
"Bash" # shell commands only
"Write|Edit|MultiEdit" # any file-writing tool
"mcp__github__.*" # all GitHub MCP tools
Omit matcher to match all tools for that event.
Project hooks (.claude/settings.json) and user hooks (~/.claude/settings.json) both run — they stack. To temporarily disable all hooks without deleting them, set "disableAllHooks": true in either file.
Sources: Hooks reference | Automate actions with hooks