Claude Code Hooks: Build Automated Dev Workflows
Claude Code Hooks beginner tutorial. Learn Hooks from scratch with 10+ practical examples, common error troubleshooting, and best practices guide.
Want Claude Code to automatically run lint, tests, and security checks? Hooks is the feature you need. This tutorial takes you from zero to mastering Hooks concepts and practical applications.
What are Hooks?
Simply put, Hooks are interceptors for Claude’s actions:
You give command → [Hook intercepts] → Claude executes → [Hook checks] → Done
You can inject your own logic at these points:
| Timing | Hook Name | Purpose |
|---|---|---|
| Before tool execution | PreToolUse | Block dangerous operations, add reminders |
| After tool execution | PostToolUse | Auto lint, run tests |
| When task completes | Stop | Ensure tests pass before ending |
| When session starts | SessionStart | Load environment, display info |
5-Minute Quick Start
Step 1: Create Config File
Create .claude/settings.json in your project directory:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo 'File written!'",
"onFailure": "ignore"
}
]
}
]
}
}
Step 2: Test the Hook
Start Claude Code:
claude
Ask Claude to create any file:
You: Create a hello.txt file with content "Hello World"
You’ll see Claude output File written! after creating the file.
Congratulations! Your first Hook is working.
Core Concepts
Hook Structure
Each Hook configuration has three parts:
{
"hooks": {
"EventName": [
{
"matcher": "Tool to intercept",
"hooks": [
{
"type": "command or prompt",
"command": "command to execute",
"onFailure": "failure handling"
}
]
}
]
}
}
Matchers
| Matcher | Description |
|---|---|
"Write" | Only intercept Write tool |
"Edit" | Only intercept Edit tool |
"Bash" | Only intercept Bash tool |
"Read" | Only intercept Read tool |
"*" | Intercept all tools |
"Write|Edit" | Intercept Write or Edit |
Hook Types
1. Command Hook (Execute commands)
{
"type": "command",
"command": "npm run lint",
"onFailure": "warn"
}
2. Prompt Hook (Use AI to judge)
{
"type": "prompt",
"prompt": "Check if this command is safe. If it contains rm -rf, return 'block'. Otherwise return 'approve'."
}
onFailure Options
| Option | Behavior |
|---|---|
"block" | Block operation on command failure |
"warn" | Show warning but continue |
"ignore" | Ignore failure |
10 Practical Examples
1. Auto-Format (Lint After Write)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"",
"onFailure": "warn"
}
]
}
]
}
}
2. Auto-Run Tests
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm test -- --passWithNoTests",
"onFailure": "warn"
}
]
}
]
}
}
3. Block Dangerous Commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Check if command contains dangerous operations (rm -rf, DROP TABLE, git push --force). If yes, return 'block'. Otherwise return 'approve'."
}
]
}
]
}
}
4. Prevent Reading Secret Files
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "prompt",
"prompt": "If file path contains .env, secret, credential, password, return 'block'. Otherwise return 'approve'."
}
]
}
]
}
}
5. Check for API Key Leaks
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "prompt",
"prompt": "Scan content for API keys (sk_, api_key, token). If found, return 'block' with explanation. Otherwise return 'approve'."
}
]
}
]
}
}
6. Force Tests to Pass Before Completion
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "npm test",
"onFailure": "block"
}
]
}
]
}
}
7. Display Session Start Message
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Claude Code started - Project: '$(basename $(pwd))",
"onFailure": "ignore"
}
]
}
]
}
}
8. TypeScript Type Check
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit",
"onFailure": "warn"
}
]
}
]
}
}
9. Git Commit Pre-Check
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "If command is git commit or git push, confirm tests have passed. If uncertain, return 'ask' to query user. Otherwise return 'approve'."
}
]
}
]
}
}
10. Sensitive Directory Protection
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "If file is in auth/, payment/, admin/ directory, return 'ask' for confirmation. Otherwise return 'approve'."
}
]
}
]
}
}
Common Error Troubleshooting
Error 1: Hook Not Triggering
Symptom: Configured Hook but it doesn’t execute
Checklist:
- Is JSON format correct? Validate with
jq . .claude/settings.json - Is Matcher spelled correctly? (
Writenotwrite) - Is file location correct? (
.claude/settings.json) - Did you restart Claude?
Solution:
# Validate JSON format
cat .claude/settings.json | jq .
# Restart Claude
claude
Error 2: Command Hook Failing
Symptom: Hook command failed with exit code X
Common Causes:
- Command path not found
- Insufficient permissions
- Command itself has errors
Solution:
# Test command manually first
npm test
# Confirm command is in PATH
which npm
Error 3: Prompt Hook Not Working as Expected
Symptom: Hook makes wrong judgment or doesn’t respond
Improvement Method:
Bad prompt:
{
"prompt": "Check if this is safe"
}
Good prompt:
{
"prompt": "Check if command contains 'rm -rf'. If yes, return 'block'. Otherwise return 'approve'. Return only one of these two values."
}
Error 4: Environment Variable Issues
Symptom: $CLAUDE_FILE_PATHS is empty
Solution: Only certain Hook events have specific variables
| Variable | Available Hooks |
|---|---|
$CLAUDE_FILE_PATHS | PostToolUse (after Write/Edit) |
$CLAUDE_PROJECT_DIR | All Hooks |
Error 5: Infinite Loop
Symptom: Hook triggers Hook, gets stuck
Cause: Command executed by Hook triggers another Hook
Solution: Use more precise matcher or avoid triggering other tools in command
Best Practices
1. Start Simple
Don’t configure too many Hooks at once. Start with one:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo 'File written!'",
"onFailure": "ignore"
}
]
}
]
}
}
2. Use onFailure Appropriately
| Scenario | Recommended Setting |
|---|---|
| Tests must pass | "onFailure": "block" |
| Lint can have warnings | "onFailure": "warn" |
| Non-critical checks | "onFailure": "ignore" |
3. Layered Configuration
PreToolUse → Prevent dangerous operations (Security)
PostToolUse → Automated checks (Quality)
Stop → Confirm completion standards (Delivery)
4. Team Sharing
Add .claude/settings.json to Git so team shares the same Hooks:
git add .claude/settings.json
git commit -m "Add Claude Code hooks for team standards"
Complete Configuration Example
Here’s a starting configuration suitable for most projects:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Check if command contains dangerous operations (rm -rf, DROP, --force). If yes, return 'block'. Otherwise return 'approve'."
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "prompt",
"prompt": "If path contains .env or secret, return 'block'. Otherwise return 'approve'."
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint -- --quiet 2>/dev/null || true",
"onFailure": "ignore"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "npm test -- --passWithNoTests 2>/dev/null",
"onFailure": "warn"
}
]
}
],
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo 'Project: '$(basename $(pwd))",
"onFailure": "ignore"
}
]
}
]
}
}
Next Steps
After learning Hooks basics, recommended reading:
- Hooks Complete Guide - Advanced configuration and more examples
- Hooks Development Manual - Script development details
- TDD Workflow - Implement TDD with Hooks
- Security Best Practices - Security-related Hook configurations
Summary
| You want to… | Use this Hook |
|---|---|
| Block dangerous operations | PreToolUse + prompt |
| Auto lint/test | PostToolUse + command |
| Ensure tests pass before completion | Stop + command |
| Execute at session start | SessionStart + command |
Remember: Hooks turn “suggestions” into “enforcement”. Configure once, effective forever.
Last updated: 2026-01-19