Claude Code Hooks Cookbook: 5 Recipes You Can Copy Today
[01]Why Hooks Matter
Every Claude Code session runs through the same silent gates: tool calls, responses, prompt submissions. Most teams treat these as untouchable. They are not. The hooks field in .claude/settings.json lets you inject shell scripts at eight lifecycle events — and those scripts can block, log, notify, or transform what Claude does next.
A hook is cheaper than a CI job, closer to the action than a pre-commit hook, and runs on your laptop so it actually gets run. If you are still manually typing npm run check after every Claude edit, stop. This is automation you wire up once and forget for good.
This cookbook walks through five recipes you can copy into .claude/settings.json today, the anatomy so you can write your own, and the four common mistakes that will burn you the first week.
[02]Hook Anatomy
Hooks live under the hooks key in .claude/settings.json. The structure is always the same:
{
"hooks": {
"<EventName>": [
{
"matcher": "<regex over tool name or empty>",
"hooks": [
{ "type": "command", "command": "sh .claude/hooks/your-script.sh" }
]
}
]
}
}
Three things matter:
- Matcher: a regex matched against the tool name (e.g.
Edit|Write,Bash, or omit for all). Only applies to tool-related events. - Command: any shell command. It inherits your shell's PATH, runs from your repo root, and gets the project directory in
$CLAUDE_PROJECT_DIR. - Exit code:
0continues,2blocks with stderr shown back to Claude, anything else is treated as a soft error.
Claude sends the event payload as JSON on stdin. A hook can read it, decide, and print output on stderr (for Claude) or stdout (quiet).
[03]The Eight Hook Events
Every Claude Code lifecycle step is a hook point. Use the right event to minimize work:
| Event | When it fires | Best for |
|---|---|---|
PreToolUse | Before any tool call | Validation, blocking dangerous commands |
PostToolUse | After a tool call | Auto-format, regenerate derived files |
UserPromptSubmit | When you press enter | Prompt audit logs, policy check |
Stop | Response finished | Desktop notification, session snapshot |
SubagentStop | A delegated sub-agent finished | Aggregate multi-agent results |
Notification | Claude shows a notification | Route to Slack, phone, or log |
PreCompact | Before auto-compact runs | Dump state, archive transcript |
SessionStart | Session begins | Load env, print reminders |
The five recipes below cover the three most-used: PreToolUse, PostToolUse, and Stop.
[04]Five Copy-Paste Recipes
Recipe 1: Gate Every Edit with a Type Check
Refuse any Edit/Write if the project does not typecheck. Forces Claude to fix compile errors before piling on.
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "sh .claude/hooks/typecheck.sh" }
]
}
]
}
}
#!/bin/sh
# .claude/hooks/typecheck.sh
npm run check 1>&2 || exit 2
exit 2 blocks the edit and shows the typecheck errors to Claude so it can fix them on the next turn.
Recipe 2: Auto-Format After Every Write
Keep style drift to zero without asking Claude to remember Prettier rules.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write --loglevel=warn $CLAUDE_PROJECT_DIR" }
]
}
]
}
}
Runs silently on success. If Prettier fails, the non-zero exit surfaces in Claude's next prompt context.
Recipe 3: Desktop Notification When Claude Stops
For long-running sessions where you alt-tab away. macOS version:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Claude finished\" with title \"claude-code\"'" }
]
}
]
}
}
Linux: swap for notify-send "claude-code" "Claude finished". Windows PowerShell: New-BurntToastNotification -Text "Claude finished".
Recipe 4: Prompt Audit Log
Every prompt you submit gets timestamped and appended to a per-project log. Useful for incident retros and "what did I ask Claude last week?" queries.
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "sh .claude/hooks/log-prompt.sh" }
]
}
]
}
}
#!/bin/sh
# .claude/hooks/log-prompt.sh
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
prompt=$(jq -r '.prompt' 2>/dev/null)
mkdir -p .claude/memory
printf '%s\t%s\n' "$ts" "$prompt" >> .claude/memory/prompts.log
The JSON on stdin carries the prompt text — jq -r '.prompt' extracts it. Log file stays out of git via .claude/memory in your gitignore.
Recipe 5: Block Dangerous Bash Commands
A safety net that prevents Claude from running anything destructive, even if a permission slips through. Applies to every Bash tool call:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "sh .claude/hooks/safety-check.sh" }
]
}
]
}
}
#!/bin/sh
# .claude/hooks/safety-check.sh
cmd=$(jq -r '.tool_input.command' 2>/dev/null)
case "$cmd" in
*"rm -rf "*|*"git push --force"*|*"sudo "*|*"curl "*"| sh"*)
echo "Blocked: $cmd" 1>&2
exit 2 ;;
esac
Pattern-matches the actual command string, exits 2 if it hits a dangerous shape. Claude sees the rejection reason in the next turn.
[05]Writing Your Own Hook
Once you see the pattern, custom hooks take about ten minutes. Every hook script follows three steps:
- Read the event payload. Claude pipes JSON to stdin. Use
jqor any JSON parser. - Decide. Pattern-match, check env, call an API — whatever gate you need.
- Exit with intent.
0continues silently.2blocks and shows your stderr to Claude. Other non-zero prints an error but usually allows continuation.
Useful env vars inside hooks:
$CLAUDE_PROJECT_DIR— absolute path to the repo root$CLAUDE_SESSION_ID— current session ID (useful for correlating logs)
For debugging, have your hook write to .claude/memory/hook-debug.log until it behaves, then silence it. Never print to stdout during a tool-matching event — Claude can misread stdout as tool output.
[06]Four Common Mistakes
1. Hooks that hang the session
If your PreToolUse hook calls a network API that times out, Claude waits. Keep synchronous hooks under two seconds. Heavy work belongs in PostToolUse (async-friendly) or in CI.
2. Hardcoding paths instead of reading stdin
A recipe that hardcodes client/src/auth.ts will break the moment Claude edits a different file. Always parse the tool input from stdin JSON — that is what the event payload is for.
3. Printing to stdout instead of stderr
During PreToolUse or PostToolUse, Claude interprets stdout as tool output in some event types. Print diagnostics to stderr (1>&2 or >&2) and keep stdout empty unless you are intentionally feeding data back.
4. Forgetting chmod +x
If your script is sh script.sh, no need. If it is ./script.sh or has a shebang and you invoke directly, chmod +x first. A silent "permission denied" is the most annoying bug in this system.
[07]Next Steps
You now have five hook recipes running. The next two pieces that compound this setup:
- The full .claude/ folder guide — everything else you can configure alongside hooks (permissions, slash commands, sub-agents).
- Claude Code's hidden features — KAIROS, ULTRAPLAN, Undercover Mode. Some pair nicely with a SessionStart hook.
- Install the MCP Buddy server — your hooks configuration works alongside MCP without conflict.
Got a hook recipe worth sharing? Start from the checker and drop it in a blog comment.
[08]Frequently Asked Questions
Can hooks be per-user only, or do they apply to everyone on the team?
Put team-wide hooks in .claude/settings.json (committed). Put personal hooks in .claude/settings.local.json (gitignored) or ~/.claude/settings.json (user scope). Local and user hooks merge with the team set — they do not replace it.
Do hooks work on Windows?
Yes, but the command runs through your shell. On Windows, use PowerShell or WSL-friendly syntax. The three cross-platform recipes above (Recipe 2, 4, 5) work as-is via WSL; Recipe 1 and 3 need platform-specific variants — see the notification recipe for the pattern.
How do I debug a hook that silently fails?
Add set -x at the top of the script and tee output to a log: exec 2> .claude/memory/hook-debug.log; set -x. You will see every command, exit code, and the stdin payload. Remove before committing.
Can a hook modify Claude's output or the tool input?
Partially. A PreToolUse hook can only allow or block — it cannot rewrite the tool input. If you exit 2 with a useful stderr message, Claude will typically retry with a corrected input. For transformation, use PostToolUse to alter files after the fact.
What is the difference between PreToolUse and UserPromptSubmit for validation?
UserPromptSubmit fires once per prompt — ideal for policy checks ("no production DB queries") before Claude plans. PreToolUse fires per tool call — ideal for granular gates like "every Edit must typecheck." Use both together for layered enforcement.