Hooks System
Automate Claude Code workflows with pre and post hooks for file changes, commands, and sessions.
What you'll accomplish
Implement custom hooks to automate your Claude Code workflow.
Prerequisites
- Completed Configuration Deep Dive lesson
- Basic shell scripting knowledge
What Are Hooks?
Hooks are scripts that run automatically at specific points in Claude Code's workflow. They let you:
- Run linting after every file change
- Execute tests before committing
- Format code automatically
- Send notifications
- Enforce coding standards
Imagine you have a rule: "Every time I put dishes in the sink, I also wipe the counter."
Hooks work the same way for Claude Code:
- "Every time Claude writes a file, run the formatter"
- "Before Claude runs a command, check if it's safe"
- "After a session ends, create a summary"
The hook runs automatically without you having to remember.
Types of Hooks
Claude Code supports several hook types:
| Hook | When It Runs | Use Case |
|---|---|---|
pre-write | Before writing a file | Validate content, check permissions |
post-write | After writing a file | Format, lint, run tests |
pre-execute | Before running a command | Security checks, logging |
post-execute | After running a command | Parse output, notifications |
pre-session | When Claude Code starts | Set up environment |
post-session | When Claude Code ends | Cleanup, summary |
pre-prompt | Before sending to Claude | Modify or enhance prompts |
Configuring Hooks
Hooks are defined in your settings file:
json{ "hooks": { "post-write": { "command": "npm run lint:fix -- {{file}}", "patterns": ["*.ts", "*.tsx"], "enabled": true }, "pre-execute": { "command": "./scripts/check-command.sh {{command}}", "enabled": true } } }
Hook Configuration Options
| Option | Description | Required |
|---|---|---|
command | The command to run | Yes |
patterns | File patterns to match (for file hooks) | No |
enabled | Whether the hook is active | No (default: true) |
timeout | Max execution time in ms | No (default: 30000) |
cwd | Working directory | No (default: project root) |
failBehavior | What to do if hook fails | No (default: "warn") |
Template Variables
Use these in your hook commands:
| Variable | Available In | Description |
|---|---|---|
{{file}} | File hooks | Path to the file |
{{files}} | File hooks | Space-separated list of files |
{{command}} | Execute hooks | The command being run |
{{output}} | Post-execute | Command output |
{{prompt}} | Prompt hooks | The user's prompt |
{{response}} | Post-session | Claude's last response |
Common Hook Patterns
Auto-Format on Write
Run Prettier after every file change:
json{ "hooks": { "post-write": { "command": "npx prettier --write {{file}}", "patterns": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.json", "*.css"], "timeout": 10000 } } }
Lint After Changes
Run ESLint and show any issues:
json{ "hooks": { "post-write": { "command": "npx eslint {{file}} --fix", "patterns": ["*.ts", "*.tsx"], "failBehavior": "warn" } } }
Run Related Tests
Run tests for files that changed:
json{ "hooks": { "post-write": { "command": "./scripts/run-related-tests.sh {{file}}", "patterns": ["src/**/*.ts"], "timeout": 60000 } } }
With a helper script (scripts/run-related-tests.sh):
bash#!/bin/bash FILE=$1 # Find related test file TEST_FILE="${FILE%.ts}.test.ts" if [ -f "$TEST_FILE" ]; then npx jest "$TEST_FILE" --passWithNoTests fi
Type Check on Write
Run TypeScript compiler to check for errors:
json{ "hooks": { "post-write": { "command": "npx tsc --noEmit", "patterns": ["*.ts", "*.tsx"], "failBehavior": "warn" } } }
Command Safety Check
Prevent dangerous commands:
json{ "hooks": { "pre-execute": { "command": "./scripts/check-command.sh {{command}}", "failBehavior": "block" } } }
With safety script (scripts/check-command.sh):
bash#!/bin/bash COMMAND="$1" # List of dangerous patterns DANGEROUS=( "rm -rf /" "rm -rf ~" "rm -rf *" ":(){ :|:& };:" "dd if=" "> /dev/sd" "mkfs" "--force" "-f origin main" ) for pattern in "${DANGEROUS[@]}"; do if [[ "$COMMAND" == *"$pattern"* ]]; then echo "BLOCKED: Command contains dangerous pattern: $pattern" exit 1 fi done echo "Command approved: $COMMAND" exit 0
Session Summary
Generate a summary when the session ends:
json{ "hooks": { "post-session": { "command": "./scripts/log-session.sh", "enabled": true } } }
With logging script (scripts/log-session.sh):
bash#!/bin/bash DATE=$(date +"%Y-%m-%d %H:%M:%S") LOG_DIR="$HOME/.claude/logs" mkdir -p "$LOG_DIR" echo "[$DATE] Session ended in $(pwd)" >> "$LOG_DIR/sessions.log" echo "Files changed:" >> "$LOG_DIR/sessions.log" git diff --name-only HEAD 2>/dev/null >> "$LOG_DIR/sessions.log" echo "---" >> "$LOG_DIR/sessions.log"
Advanced Hook Examples
Notify on Completion
Send a notification when Claude finishes a long task:
Auto-Commit Changes
Commit changes after each file write:
json{ "hooks": { "post-write": { "command": "./scripts/auto-commit.sh {{file}}", "patterns": ["*.ts", "*.tsx"] } } }
With commit script (scripts/auto-commit.sh):
bash#!/bin/bash FILE=$1 # Only commit if there are changes if [ -n "$(git status --porcelain $FILE)" ]; then git add "$FILE" git commit -m "Auto-commit: Update $FILE" fi
Warning
Auto-committing can create noisy Git history. Use sparingly, and consider squashing commits later.
Enforce File Structure
Block writes to forbidden locations:
json{ "hooks": { "pre-write": { "command": "./scripts/check-file-path.sh {{file}}", "failBehavior": "block" } } }
With path checker (scripts/check-file-path.sh):
bash#!/bin/bash FILE=$1 # Block writes outside the src directory if [[ ! "$FILE" == src/* && ! "$FILE" == tests/* && ! "$FILE" == *.md ]]; then echo "BLOCKED: Can only write to src/, tests/, or markdown files" exit 1 fi # Block writes to generated files if [[ "$FILE" == *generated* || "$FILE" == *dist/* ]]; then echo "BLOCKED: Cannot modify generated files" exit 1 fi exit 0
Dynamic Prompting
Enhance prompts with project context:
json{ "hooks": { "pre-prompt": { "command": "./scripts/enhance-prompt.sh" } } }
This is an advanced pattern where your script outputs additional context that gets prepended to the user's prompt.
Fail Behaviors
The failBehavior option controls what happens when a hook fails:
| Value | Behavior |
|---|---|
"ignore" | Continue silently |
"warn" | Show warning but continue |
"block" | Stop the operation |
"retry" | Try the hook again (up to 3 times) |
Choose based on the hook's importance:
- Formatting:
"warn"(nice to have) - Security checks:
"block"(critical) - Tests:
"warn"or"block"(depends on workflow)
Debugging Hooks
Enable Verbose Mode
See hook execution details:
bashclaude --verbose
Test Hooks Manually
Run your hook script directly to debug:
bash./scripts/check-command.sh "npm install" echo $? # Check exit code
Check Hook Logs
Claude Code logs hook execution. Check:
bashcat ~/.claude/logs/hooks.log
Common Issues
Frequently Asked Questions
Check:
- Hook is
enabled: true(or omitted - defaults to true) - File pattern matches (use
echo {{file}}to debug) - Script is executable (
chmod +x script.sh) - Settings file is valid JSON
- Test the command manually in terminal
- Check that paths are correct (use absolute paths if needed)
- Verify template variables are expanding correctly
- Check the script's exit code
Check:
- Script exit code (0 = success, non-zero = failure)
failBehaviorsetting- Error output from the script
- Add a
timeoutvalue - Run expensive operations asynchronously (background with
&) - Only match specific patterns (don't run on every file)
- Cache results where possible
Best Practices
1. Keep Hooks Fast
Hooks run synchronously. Slow hooks make Claude Code feel sluggish. Target under 5 seconds for post-write hooks.
2. Use Exit Codes Correctly
exit 0= successexit 1= failure (will trigger failBehavior)
3. Be Specific with Patterns
Instead of running on all files:
json"patterns": ["*.ts", "*.tsx"]
Not:
json"patterns": ["*"]
4. Log for Debugging
Write to a log file in complex hooks:
bashecho "[$(date)] Hook ran on $FILE" >> ~/.claude/hooks.log
5. Test Before Deploying
Test hooks locally before adding to project settings. A broken hook affects everyone.
Verify Your Understanding
You've completed this lesson if you can:
Next: Understanding Claude Code's permission system and when to use --dangerously-skip-permissions.