Claude Code Hooks Tutorials & Examples
Progressive tutorials from beginner to advanced with practical examples.
Workshop Overview
- Duration: 5 hours full workshop (2.5 hours express)
- Prerequisites: Basic command line, Claude Code familiarity
- Goal: Master hooks from basics to advanced patterns
Getting Started
Environment Setup
# Create practice project
mkdir claude-hooks-workshop
cd claude-hooks-workshop
mkdir .claude
# Create initial configuration
echo '{"hooks": {}}' > .claude/settings.json
# Open Claude Code and verify
claude-code
# Run: /hooksRequired Tools
jq- JSON processorcurl- HTTP clientgit- Version controlpython3- For advanced examples (optional)
Beginner Examples (45 min)
1. Your First Hook - Basic Logging
{
"hooks": {
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "echo 'Claude used a tool!' >> claude-log.txt"
}
]
}
]
}
}2. Desktop Notifications
macOS:
{
"hooks": {
"Stop": [{
"hooks": [{
"command": "osascript -e 'display notification \"Task completed!\" with title \"Claude Code\"'"
}]
}]
}
}Linux:
{
"hooks": {
"Stop": [{
"hooks": [{
"command": "notify-send 'Claude Code' 'Task completed!'"
}]
}]
}
}3. File Backup Before Edits
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|MultiEdit",
"hooks": [{
"command": "for file in $CLAUDE_FILE_PATHS; do cp \"$file\" \"$file.backup\"; done"
}]
}
]
}
}4. Activity Tracking
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"command": "echo \"$(date): Modified $CLAUDE_FILE_PATHS\" >> ~/.claude/activity.log"
}]
}
]
}
}5. Simple Validation
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{
"command": "if [[ \"$CLAUDE_FILE_PATHS\" == *\".env\"* ]]; then echo 'Cannot modify .env files!' >&2; exit 2; fi"
}]
}
]
}
}Exercise: Timestamped Backups ⭐⭐
Create backups with timestamps instead of .backup.
Solution
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|MultiEdit",
"hooks": [{
"command": "for file in $CLAUDE_FILE_PATHS; do cp \"$file\" \"$file.backup.$(date +%Y%m%d_%H%M%S)\"; done"
}]
}
]
}
}Intermediate Examples (60 min)
1. Multi-Language Code Formatter
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"command": "for file in $CLAUDE_FILE_PATHS; do case \"$file\" in *.js|*.jsx|*.ts|*.tsx) prettier --write \"$file\" ;; *.py) black \"$file\" ;; *.go) gofmt -w \"$file\" ;; *.rs) rustfmt \"$file\" ;; esac; done"
}]
}
]
}
}2. Type Checking & Linting
#!/bin/bash
# Save as: .claude/hooks/type_check.sh
for file in $CLAUDE_FILE_PATHS; do
if [[ "$file" =~ \.(ts|tsx)$ ]]; then
echo "Type checking $file..."
npx tsc --noEmit --skipLibCheck "$file" || echo "⚠️ Type errors in $file"
echo "Linting $file..."
eslint --fix "$file" || echo "⚠️ Lint errors in $file"
fi
done3. Test Runner for Modified Files
#!/usr/bin/env python3
# Save as: .claude/hooks/smart_test.py
import json
import sys
import os
import subprocess
from pathlib import Path
def find_test_file(source_file):
path = Path(source_file)
test_patterns = [
path.parent / f"test_{path.stem}.py",
path.parent / f"{path.stem}_test.py",
path.parent / "tests" / f"test_{path.stem}.py"
]
for test_path in test_patterns:
if test_path.exists():
return str(test_path)
return None
data = json.load(sys.stdin)
files = os.environ.get('CLAUDE_FILE_PATHS', '').split()
for file in files:
if 'test' not in file:
test_file = find_test_file(file)
if test_file:
print(f"Running tests for {file}...")
subprocess.run(['pytest', test_file, '-v'])4. Git Pre-commit Integration
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"command": "if echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.command' | grep -q 'git commit'; then prettier --write '**/*.{js,ts,json}' 2>/dev/null || true; npm test || exit 2; fi"
}]
}
]
}
}Exercise: Quality Gate ⭐⭐⭐
Create a comprehensive quality check that formats, lints, and tests code.
Solution
#!/bin/bash
# Save as: .claude/hooks/quality_gate.sh
ERRORS=0
# Format
for file in $CLAUDE_FILE_PATHS; do
case "$file" in
*.py) black "$file" 2>/dev/null || ((ERRORS++)) ;;
*.js|*.ts) prettier --write "$file" 2>/dev/null || ((ERRORS++)) ;;
esac
done
# Lint
for file in $CLAUDE_FILE_PATHS; do
case "$file" in
*.py) pylint "$file" --exit-zero ;;
*.js|*.ts) eslint "$file" --fix 2>/dev/null ;;
esac
done
# Test
npm test 2>/dev/null || ((ERRORS++))
if [ $ERRORS -gt 0 ]; then
echo "Quality gate failed with $ERRORS errors"
exit 2
fiAdvanced Examples (90 min)
1. CI/CD Pipeline Integration
#!/bin/bash
# Automated PR creation with checks
if git diff --quiet && git diff --staged --quiet; then
echo 'No changes'
exit 0
fi
BRANCH="claude-$(date +%s)"
git checkout -b "$BRANCH"
git add -A
git commit -m "Claude Code: Automated improvements"
git push -u origin "$BRANCH"
gh pr create \
--title "Claude Code: Automated improvements" \
--body "Automated changes by Claude Code" \
--label "claude-code"2. Multi-Service Orchestration
#!/usr/bin/env python3
import json
import sys
import subprocess
from pathlib import Path
def detect_service(file_path):
"""Detect which service a file belongs to"""
path = Path(file_path)
service_map = {
'frontend': ['src/components', 'src/pages'],
'api': ['api/', 'server/'],
'database': ['migrations/', 'models/']
}
for service, patterns in service_map.items():
for pattern in patterns:
if pattern in str(path):
return service
return 'unknown'
data = json.load(sys.stdin)
files = os.environ.get('CLAUDE_FILE_PATHS', '').split()
affected_services = set()
for file in files:
service = detect_service(file)
affected_services.add(service)
# Handle service-specific tasks
if 'frontend' in affected_services:
subprocess.run(['npm', 'run', 'build'])
if 'api' in affected_services:
subprocess.run(['npm', 'test', '--', '--testPathPattern=api'])3. Security Scanning
#!/usr/bin/env python3
import json
import sys
import subprocess
data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')
# Security checks
if file_path.endswith('.py'):
result = subprocess.run(
['bandit', '-f', 'json', file_path],
capture_output=True,
text=True
)
if result.returncode != 0:
issues = json.loads(result.stdout)
if issues.get('results'):
print(json.dumps({
'continue': False,
'stopReason': f"Security issues found: {len(issues['results'])}"
}))
sys.exit(2)Workshop Exercises
Beginner Challenges
-
Time-stamped Notifications ⭐
- Modify notifications to include current time
-
Daily Summary ⭐⭐
- Create a daily activity summary
-
File Size Check ⭐⭐
- Block edits to files over 1MB
Intermediate Challenges
-
JSON/YAML Formatter ⭐⭐⭐
- Add JSON and YAML to the formatter
-
Smart Test Runner ⭐⭐⭐
- Run only tests for modified source files
-
Coverage Check ⭐⭐⭐⭐
- Ensure code coverage stays above 80%
Advanced Challenges
-
Auto-merge CI ⭐⭐⭐⭐
- Create PRs that auto-merge if tests pass
-
Blue-Green Deploy ⭐⭐⭐⭐⭐
- Implement zero-downtime deployments
-
Observability System ⭐⭐⭐⭐⭐
- Build complete monitoring with metrics
Tips for Success
- Start Simple: Test basic hooks before complex ones
- Use Logging: Debug with comprehensive logs
- Handle Errors: Always include error handling
- Test Manually: Run commands outside hooks first
- Document: Explain what your hooks do
Quick Reference Card
Hook Events
PreToolUse- Before tools (can block)PostToolUse- After toolsStop- When Claude finishesNotification- On notificationsSubagentStop- Subagent completion
Exit Codes
0- Success, continue2- Block operation- Other - Warning, continue
Environment Variables
$CLAUDE_TOOL_NAME$CLAUDE_FILE_PATHS$CLAUDE_TOOL_INPUT$CLAUDE_NOTIFICATION
Next Steps
- Review Design Patterns
- Explore Third-Party Integrations
- Study Security Best Practices
- Debug with Troubleshooting Guide