Curriculum path
- CLAUDE.md Mastery — repo memory and rules
- Effective Prompting — task framing and constraints
- MCP Power Tools — connect tools and live context
- Multi-Agent Workflows — delegation and parallel execution
- Hooks Automation — local workflow enforcement ← You are here
- GitHub Actions Workflows — move repeatable work into team automation
Official docs used in this guide
- Hook lifecycle and configuration model — Hooks
- Security considerations for hook scripts — Hooks
- Settings surface and local configuration — Settings
Why Hooks Matter
Claude Code gets much more useful once it stops being only a smart editor and starts enforcing your team's workflow automatically.
Hooks let Claude run shell commands around tool usage and lifecycle events. That means you can wire in:
- formatting after edits
- lint or test checks before risky operations
- notifications when long tasks finish
- guards that block edits in sensitive paths
- context preservation before compaction
In practice, hooks turn repeated review habits into automation.
The Hook Lifecycle
Every time Claude Code runs a tool, this flow happens:
User request
↓
PreToolUse hook fires ← block or pre-check here
↓
Tool executes (Edit, Write, Bash, etc.)
↓
PostToolUse hook fires ← post-process, format, notify here
↓
Claude responds
If a PreToolUse hook exits with a non-zero code, the tool is blocked entirely. That is the mechanism behind file protection guardrails.
Configuring Hooks
Hooks are registered in .claude/settings.json at the project root. Create the file if it does not already exist.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": ".claude/hooks/protect-files.sh"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": ".claude/hooks/auto-format.sh"
}
],
"PreCompact": [
{
"matcher": ".*",
"command": ".claude/hooks/save-context.sh"
}
],
"Notification": [
{
"matcher": ".*",
"command": ".claude/hooks/notify-done.sh"
}
]
}
}- matcher: a regex that filters which tools trigger the hook.
"Edit|Write"matches both the Edit and Write tools. - command: the shell command or script path to execute.
- Hook scripts need execute permission:
chmod +x .claude/hooks/your-script.sh.
Practical Hook Examples
1. Auto-format after edits (PostToolUse)
Run Prettier automatically every time a file is modified. This example targets TypeScript and TSX files only.
#!/bin/bash
# .claude/hooks/auto-format.sh
FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path // empty')
# Exit quietly if no file path was provided
[[ -z "$FILE" ]] && exit 0
# Apply Prettier to TypeScript files only
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
npx prettier --write "$FILE" 2>/dev/null
echo "Formatted: $FILE"
fiThe CLAUDE_TOOL_INPUT environment variable contains the tool's input as JSON. Use jq to pull out what you need.
2. Protect sensitive files (PreToolUse)
Block Claude from accidentally editing .env files, migration history, or generated code.
#!/bin/bash
# .claude/hooks/protect-files.sh
FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path // empty')
# List of path patterns to protect
PROTECTED=(
".env"
".env.local"
".env.production"
"prisma/migrations"
"generated/"
"__generated__"
)
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE" == *"$pattern"* ]]; then
echo "BLOCKED: '$FILE' is a protected file. Edit manually if this is intentional." >&2
exit 1
fi
done
exit 0Exiting with 1 stops the tool. Exiting with 0 lets it proceed.
3. Preserve context before compaction (PreCompact)
When a conversation grows long, Claude Code compacts older content. Save a snapshot before that happens so you can pick up where you left off.
#!/bin/bash
# .claude/hooks/save-context.sh
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
SAVE_DIR=".claude/snapshots"
mkdir -p "$SAVE_DIR"
# Write a context snapshot
cat > "$SAVE_DIR/context-$TIMESTAMP.md" << EOF
# Context Snapshot — $TIMESTAMP
## Git status
$(git status --short 2>/dev/null || echo "no git")
## Recently changed files
$(git diff --name-only HEAD 2>/dev/null | head -20 || echo "none")
## Notes
Auto-saved before compaction.
EOF
echo "Context saved to $SAVE_DIR/context-$TIMESTAMP.md"4. Desktop notification when a task finishes (Notification)
You should not have to watch the terminal the whole time. Send a desktop notification when Claude is done.
#!/bin/bash
# .claude/hooks/notify-done.sh
# macOS
if command -v osascript &>/dev/null; then
osascript -e 'display notification "Claude Code finished the task" with title "Claude Code" sound name "Glass"'
fi
# Linux (notify-send)
if command -v notify-send &>/dev/null; then
notify-send "Claude Code" "Task complete"
fiThe Mental Model
Think of hooks as event-driven guardrails.
Claude Code exposes lifecycle points such as:
- before a tool runs
- after a tool runs
- before compaction
- after compaction
That gives you a way to inject your own checks without repeating the same prompt every session.
High-Value Uses
1. Protect critical files
Block accidental edits to files like:
.env- production deployment config
- generated code
- migration history
2. Auto-run checks
After edit-heavy operations, trigger:
eslinttsc --noEmit- targeted tests
- formatter checks
3. Preserve context
Before compaction, save useful state into:
- task notes
- summary files
- progress artifacts
4. Send notifications
For long-running tasks, a hook can notify you when Claude is done instead of making you babysit the terminal.
Debugging Tips
When a hook does not behave as expected, try these approaches.
1. Run the script standalone
# Simulate the environment variable and run directly
export CLAUDE_TOOL_INPUT='{"file_path": "src/app.ts"}'
bash .claude/hooks/auto-format.sh
echo "Exit code: $?"2. Log to stderr
Writing to >&2 inside a hook script surfaces output in Claude Code's error stream.
echo "DEBUG: FILE=$FILE" >&23. Check exit codes
A non-zero exit from a PreToolUse hook blocks the tool. If Claude is getting unexpectedly blocked, check what your script exits with.
4. Verify jq is installed
which jq || echo "jq not found — brew install jq or apt install jq"Security Matters More Here
Anthropic is very explicit: hooks execute arbitrary shell commands on your system.
That means hooks are powerful, but also dangerous if written carelessly.
Security habits that matter:
- quote shell variables
- validate inputs
- use absolute paths
- block path traversal
- skip secrets and sensitive files
- test hooks in a safe environment first
A sloppy hook can be more dangerous than a sloppy prompt.
Complete Hook Event Reference
Here is a comprehensive table of every hook event type Claude Code supports.
| Event | Timing | Use Case |
|---|---|---|
PreToolUse |
Before tool executes | Block dangerous operations, validate inputs |
PostToolUse |
After tool executes | Auto-format, lint, send notifications |
PreCompact |
Before context compression | Save important state, create snapshots |
PostCompact |
After context compression | Restore context, re-inject critical rules |
Notification |
When Claude sends a notification | Desktop alerts, Slack messages, sound effects |
Stop |
When Claude finishes responding | Session cleanup, final validation, auto-save |
Key points:
- Each event receives different environment variables
PreToolUsecan BLOCK execution (exit 1). All others are advisory only- Matcher patterns support regex:
"Edit|Write","Bash",".*"for all tools
Environment Variable Reference
These are the environment variables available inside hook scripts.
| Variable | Available In | Contains |
|---|---|---|
CLAUDE_TOOL_NAME |
All hooks | Name of the tool being used (Edit, Write, Bash, etc.) |
CLAUDE_TOOL_INPUT |
All hooks | JSON with tool parameters (file_path, command, etc.) |
CLAUDE_TOOL_OUTPUT |
PostToolUse only | JSON with tool execution result |
CLAUDE_SESSION_ID |
All hooks | Current session identifier |
CLAUDE_PROJECT_DIR |
All hooks | Project root directory path |
CLAUDE_MODEL |
All hooks | Current model (claude-sonnet-4-6, etc.) |
- Parse JSON inputs with
jq:echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path' - Always handle missing variables gracefully with defaults
PostCompact Hook Pattern
Context compression removes older conversation history to free up tokens. The problem is that important rules or context can disappear along with it.
The PostCompact hook runs after compression finishes and can re-inject critical context back into the conversation.
#!/bin/bash
# .claude/hooks/post-compact.sh
# Re-inject critical context after compression
cat << 'CONTEXT'
[Post-Compact Context Restoration]
- Current task: Review authentication module
- Important constraint: Do NOT modify database schema
- Active branch: feature/auth-refactor
- Files modified so far: src/auth/login.ts, src/auth/session.ts
CONTEXTRegister it in settings:
{
"hooks": {
"PostCompact": [
{
"matcher": ".*",
"command": ".claude/hooks/post-compact.sh"
}
]
}
}- Output from PostCompact hooks is fed back into the conversation as context
- Keep it short — long outputs defeat the purpose of compaction
Multi-Hook Workflow: Local CI Pipeline
Combine multiple hooks to build a complete local CI pipeline. Every time a file is edited, type checking, linting, and tests run automatically.
#!/bin/bash
# .claude/hooks/local-ci.sh — PostToolUse hook for Edit|Write
FILE=$(echo "$CLAUDE_TOOL_INPUT" | jq -r '.file_path // empty')
[[ -z "$FILE" ]] && exit 0
ERRORS=""
# Step 1: TypeScript type checking (only for .ts/.tsx files)
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
if ! npx tsc --noEmit --pretty 2>/tmp/tsc-output.txt; then
ERRORS+="TypeScript errors found:\n$(cat /tmp/tsc-output.txt)\n\n"
fi
fi
# Step 2: ESLint check
if [[ "$FILE" == *.ts || "$FILE" == *.tsx || "$FILE" == *.js ]]; then
if ! npx eslint "$FILE" --quiet 2>/tmp/eslint-output.txt; then
ERRORS+="ESLint issues:\n$(cat /tmp/eslint-output.txt)\n\n"
fi
fi
# Step 3: Run related tests
TEST_FILE="${FILE%.ts}.test.ts"
if [[ -f "$TEST_FILE" ]]; then
if ! npx jest "$TEST_FILE" --silent 2>/tmp/test-output.txt; then
ERRORS+="Test failures:\n$(cat /tmp/test-output.txt)\n\n"
fi
fi
# Report results
if [[ -n "$ERRORS" ]]; then
echo "⚠ Local CI found issues:" >&2
echo -e "$ERRORS" >&2
else
echo "✓ Local CI passed: types, lint, tests" >&2
fi
exit 0 # Don't block — just reportKey points:
- This runs automatically after every file edit
- Reports via stderr so Claude sees the feedback
- exit 0 means advisory — Claude sees the warnings but is not blocked
- Change to exit 1 in PreToolUse if you want to enforce
Complete settings.json for the full pipeline:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": ".claude/hooks/protect-files.sh"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": ".claude/hooks/auto-format.sh"
},
{
"matcher": "Edit|Write",
"command": ".claude/hooks/local-ci.sh"
}
],
"PreCompact": [
{
"matcher": ".*",
"command": ".claude/hooks/save-context.sh"
}
],
"PostCompact": [
{
"matcher": ".*",
"command": ".claude/hooks/post-compact.sh"
}
],
"Notification": [
{
"matcher": ".*",
"command": ".claude/hooks/notify-done.sh"
}
]
}
}Hooks vs CLAUDE.md
| Tool | Best for |
|---|---|
CLAUDE.md |
policy, architecture, coding rules |
| hooks | automatic enforcement and checks |
| prompt | today's task |
When Not to Use Hooks
Avoid hooks when the workflow is still changing quickly, when the commands are too slow to run constantly, or when the logic depends on nuanced human judgment.
Claude Code vs Codex
Codex usually leans on sandbox and approval boundaries. Claude Code hooks lean on local event-driven automation.