10 Hook Patterns

The building blocks for every Claude Code hook. Learn these patterns, combine them for any use case.

1. Block

Exit 2 to prevent a tool call
Use: PreToolUse · When you want to stop something dangerous
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
    echo "BLOCKED: rm -rf" >&2
    exit 2
fi
exit 0

2. Approve

JSON stdout to skip permission prompt
Use: PreToolUse · When a command is always safe
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
case "$(echo "$COMMAND" | awk '{print $1}')" in
    cat|ls|grep|find)
        echo '{"decision":"approve","reason":"Read-only"}'
        exit 0 ;;
esac
exit 0

3. Warn

stderr message without blocking
Use: PostToolUse · When you want Claude to see a reminder
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'git\s+commit'; then
    echo "REMINDER: Did you run tests?" >&2
fi
exit 0

4. Log

Record events to a file
Use: PostToolUse · When you need an audit trail
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
echo "[$(date -Iseconds)] $TOOL: ${CMD:0:80}" >> ~/.claude/activity.log
exit 0

5. Validate

Check result quality after execution
Use: PostToolUse · When you want to catch errors
#!/bin/bash
FILE=$(cat | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
case "$FILE" in
    *.py) python3 -m py_compile "$FILE" 2>&1 || echo "SYNTAX ERROR: $FILE" >&2 ;;
    *.json) python3 -m json.tool "$FILE" >/dev/null 2>&1 || echo "BAD JSON: $FILE" >&2 ;;
esac
exit 0

6. Protect

Guard specific files from modification
Use: PreToolUse on Edit|Write · When a file must not change
#!/bin/bash
FILE=$(cat | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
if [[ "$FILE" == *".env" ]]; then
    jq -n '{"decision":"block","reason":"Protected file"}'
    exit 0
fi
exit 0

7. Limit

Cap frequency or quantity
Use: PreToolUse · When too many of something is dangerous
#!/bin/bash
TRACKER="$HOME/.claude/agent-count"
COUNT=$(cat "$TRACKER" 2>/dev/null || echo 0)
if [ "$COUNT" -ge 5 ]; then
    echo "BLOCKED: Too many agents" >&2; exit 2
fi
echo $((COUNT + 1)) > "$TRACKER"
exit 0

8. Checkpoint

Save state before risky operations
Use: PreToolUse on Edit|Write · For rollback capability
#!/bin/bash
FILE=$(cat | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
mkdir -p .claude/checkpoints
cp "$FILE" ".claude/checkpoints/$(basename "$FILE").$(date +%s).bak"
exit 0

9. Enforce

Re-inject rules after context loss
Use: PostToolUse · When CLAUDE.md rules get forgotten
#!/bin/bash
COMMAND=$(cat | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -qE 'git\s+push.*(main|master)'; then
    echo "RULE: CLAUDE.md prohibits pushing to main" >&2
fi
exit 0

10. Recover

Auto-fix or alert on failure
Use: Stop · When the session ends unexpectedly
#!/bin/bash
INPUT=$(cat)
REASON=$(echo "$INPUT" | jq -r '.stop_reason // empty')
if [ "$REASON" = "error" ]; then
    # Send notification
    echo "Session ended with error" >> ~/.claude/alerts.log
fi
exit 0