Hooks System Mastery
Your junior developer just asked Claude Code to “clean up the database migration files.” Claude interpreted that as deleting the migration history. The production deploy script ran the next morning and recreated every table from scratch. Your Friday night is now a recovery operation.
Hooks prevent this. They are deterministic shell commands, LLM prompts, or agent invocations that fire at specific points in Claude Code’s lifecycle. They do not depend on the model following instructions — they execute whether the model cooperates or not.
What You Will Walk Away With
Section titled “What You Will Walk Away With”- A working mental model of the 14 hook events and when each fires
- Production-ready hook scripts for file protection, auto-formatting, and security enforcement
- Patterns for prompt-based and agent-based hooks that use AI judgment deterministically
- Debugging techniques when hooks behave unexpectedly
The Hook Lifecycle
Section titled “The Hook Lifecycle”Every Claude Code session follows a predictable lifecycle. Hooks intercept this lifecycle at 14 distinct points:
| Event | When It Fires | Can Block? |
|---|---|---|
SessionStart | Session begins or resumes | Yes |
UserPromptSubmit | After you press Enter, before Claude processes | Yes |
PreToolUse | Before any tool call executes | Yes |
PermissionRequest | When a permission dialog would appear | Yes |
PostToolUse | After a tool call succeeds | No |
PostToolUseFailure | After a tool call fails | No |
Notification | When Claude sends a notification | No |
SubagentStart | When a subagent spawns | No |
SubagentStop | When a subagent finishes | No |
Stop | When Claude finishes responding | Yes |
TeammateIdle | When an agent team member goes idle | Yes |
TaskCompleted | When a task is marked complete | Yes |
PreCompact | Before context compaction | No |
SessionEnd | When the session terminates | No |
The “Can Block?” column is key. PreToolUse hooks can prevent a dangerous command from executing. Stop hooks can force Claude to keep working when it tries to finish prematurely.
Configuration
Section titled “Configuration”Hooks live in your settings JSON files. The configuration has three levels:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-dangerous-commands.sh" } ] } ] }}Where to put hooks:
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | Current project, all team members | Yes (commit it) |
.claude/settings.local.json | Current project, just you | No (gitignored) |
Matcher Patterns
Section titled “Matcher Patterns”The matcher field is a regex that filters when the hook fires:
"Bash"— only Bash tool calls"Edit|Write"— file modification tools"mcp__.*"— any MCP tool- Omit
matcheror use"*"to match everything
Building Production Hooks
Section titled “Building Production Hooks”Auto-Format After File Changes
Section titled “Auto-Format After File Changes”This PostToolUse hook runs Prettier on any file that Claude edits or writes:
#!/bin/bash# Runs after Edit or Write tool calls
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // empty')
if [ -n "$FILE_PATH" ] && [ -f "$FILE_PATH" ]; then case "$FILE_PATH" in *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md) npx prettier --write "$FILE_PATH" 2>/dev/null ;; esacfi
exit 0Hook configuration:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/auto-format.sh" } ] } ] }}Notification Sound on Completion
Section titled “Notification Sound on Completion”{ "hooks": { "Stop": [ { "matcher": "", "hooks": [ { "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" } ] } ], "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "afplay /System/Library/Sounds/Ping.aiff" } ] } ] }}Protect Critical Files
Section titled “Protect Critical Files”Block Claude from modifying files that should only be changed through specific processes:
#!/bin/bashFILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // empty')PROTECTED_PATTERNS=( "*/migrations/*" ".env*" "*/secrets/*" "package-lock.json" "yarn.lock")
for pattern in "${PROTECTED_PATTERNS[@]}"; do if [[ "$FILE_PATH" == $pattern ]]; then jq -n "{hookSpecificOutput:{hookEventName:\"PreToolUse\",permissionDecision:\"deny\",permissionDecisionReason:\"Protected file: $FILE_PATH cannot be modified by Claude\"}}" exit 0 fidone
exit 0Prompt-Based Hooks
Section titled “Prompt-Based Hooks”When you need AI judgment rather than pattern matching, use prompt-based hooks. These send the event context to an LLM and act on the response:
{ "hooks": { "Stop": [ { "matcher": "", "hooks": [ { "type": "prompt", "prompt": "Review the assistant's final response. Did it actually complete the requested task, or did it stop early with a vague summary? If the task is incomplete, respond with {\"decision\": \"block\", \"reason\": \"Task not complete: [specific missing items]\"}. If complete, respond with {\"decision\": \"allow\"}." } ] } ] }}This pattern catches Claude’s tendency to declare victory before finishing the actual work.
Async Hooks
Section titled “Async Hooks”Some hooks should not block the main flow. Background test execution is the classic example:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/run-tests-async.sh", "async": true } ] } ] }}Async hooks start immediately but do not block Claude from continuing. The result is delivered when available.
Debugging Hooks
Section titled “Debugging Hooks”When hooks misbehave, use the debug flag:
claude --debug "hooks"This shows every hook event, matcher evaluation, and handler execution. Common issues:
- Hook not firing: Check that your matcher regex actually matches the tool name.
"Bash"matches the Bash tool, not"bash"(case-sensitive). - Hook fires but has no effect: Check the exit code and JSON output. Exit code 0 means “allow.” Exit code 2 means “block.” Anything else is an error.
- Hook script permissions: Make sure
.claude/hooks/*.shfiles are executable (chmod +x).
When This Breaks
Section titled “When This Breaks”Hooks slow down every operation: Each synchronous hook adds latency to the tool call it intercepts. If your PostToolUse hook runs a full test suite on every edit, Claude will feel sluggish. Move heavy operations to async hooks.
Hooks conflict with each other: When multiple hooks fire on the same event, they all run. If one allows and another denies, the deny wins. Use /hooks to see all configured hooks and their sources.
Prompt hooks consume extra tokens: Prompt-based hooks make additional API calls. Each one adds cost. Use them sparingly and prefer command hooks for pattern-matching tasks.
Hook cannot read stdin: Command hooks receive JSON on stdin. If your script reads from a file or does not process stdin, it will not receive the event context. Always use jq to parse the input.
What is Next
Section titled “What is Next”- Custom Commands — Build reusable commands that complement your hooks
- Memory System — Configure what Claude knows to reduce the need for hooks
- Monitoring and Costs — Track hook execution and its impact on token usage