From xpowers
Guides creation of Claude Code hooks with progressive enhancement: observe, automate, then enforce. Covers hook patterns, lifecycle events, and safe automation strategies.
How this skill is triggered — by the user, by Claude, or both
Slash command
/xpowers:building-hooksThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
<skill_overview>
<skill_overview> Hooks encode business rules at application level; start with observation, add automation, enforce only when patterns clear. </skill_overview>
<rigidity_level> MEDIUM FREEDOM - Follow progressive enhancement (observe → automate → enforce) strictly. Hook patterns are adaptable, but always start non-blocking and test thoroughly. </rigidity_level>
<quick_reference>
| Phase | Approach | Example |
|---|---|---|
| 1. Observe | Non-blocking, report only | Log edits, display reminders |
| 2. Automate | Background tasks, non-blocking | Auto-format, run builds |
| 3. Enforce | Blocking only when necessary | Block dangerous ops, require fixes |
Most used events: UserPromptSubmit (before processing), Stop (after completion)
Critical: Start Phase 1, observe for a week, then Phase 2. Only add Phase 3 if absolutely necessary. </quick_reference>
<when_to_use> Use hooks for:
Never use hooks for:
<hook_lifecycle_events>
| Event | When Fires | Use Cases |
|---|---|---|
| UserPromptSubmit | Before Claude processes prompt | Validation, context injection, skill activation |
| Stop | After Claude finishes | Build checks, formatting, quality reminders |
| PostToolUse | After each tool execution | Logging, tracking, validation |
| PreToolUse | Before tool execution | Permission checks, validation |
| ToolError | When tool fails | Error handling, fallbacks |
| SessionStart | New session begins | Environment setup, context loading |
| SessionEnd | Session closes | Cleanup, logging |
| Error | Unhandled error | Error recovery, notifications |
| </hook_lifecycle_events> |
<progressive_enhancement>
Goal: Understand patterns before acting
Examples:
Duration: Observe for 1 week minimum
Goal: Automate tedious tasks
Examples:
Requirement: Fast (<2 seconds), non-blocking
Goal: Prevent errors, enforce standards
Examples:
Requirement: Only add when patterns clear from Phase 1-2 </progressive_enhancement>
<common_hook_patterns>
Problem: TypeScript errors left behind
Solution:
#!/bin/bash
# Stop hook - runs after Claude finishes
# Check modified repos
modified_repos=$(grep -h "edited" ~/.claude/edit-log.txt | cut -d: -f1 | sort -u)
for repo in $modified_repos; do
echo "Building $repo..."
cd "$repo" && npm run build 2>&1 | tee /tmp/build-output.txt
error_count=$(grep -c "error TS" /tmp/build-output.txt || echo "0")
if [ "$error_count" -gt 0 ]; then
if [ "$error_count" -ge 5 ]; then
echo "⚠️ Found $error_count errors - consider error-resolver agent"
else
echo "🔴 Found $error_count TypeScript errors:"
grep "error TS" /tmp/build-output.txt
fi
else
echo "✅ Build passed"
fi
done
Configuration:
{
"event": "Stop",
"command": "~/.claude/hooks/build-checker.sh",
"description": "Run builds on modified repos",
"blocking": false
}
Result: Zero errors left behind
Problem: Inconsistent formatting
Solution:
#!/bin/bash
# Stop hook - format all edited files
edited_files=$(tail -20 ~/.claude/edit-log.txt | grep "^/" | sort -u)
for file in $edited_files; do
repo_dir=$(dirname "$file")
while [ "$repo_dir" != "/" ]; do
if [ -f "$repo_dir/.prettierrc" ]; then
echo "Formatting $file..."
cd "$repo_dir" && npx prettier --write "$file"
break
fi
repo_dir=$(dirname "$repo_dir")
done
done
echo "✅ Formatting complete"
Result: All code consistently formatted
Problem: Claude forgets error handling
Solution:
#!/bin/bash
# Stop hook - gentle reminder
edited_files=$(tail -20 ~/.claude/edit-log.txt | grep "^/")
risky_patterns=0
for file in $edited_files; do
if grep -q "try\|catch\|async\|await\|prisma\|router\." "$file"; then
((risky_patterns++))
fi
done
if [ "$risky_patterns" -gt 0 ]; then
cat <<EOF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 ERROR HANDLING SELF-CHECK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ Risky Patterns Detected
$risky_patterns file(s) with async/try-catch/database operations
❓ Did you add proper error handling?
❓ Are errors logged appropriately?
💡 Consider: Sentry.captureException(), proper logging
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
fi
Result: Claude self-checks without blocking
See: xpowers:skills-auto-activation for complete implementation
Summary: Analyzes prompt keywords, injects skill activation reminder before Claude processes. </common_hook_patterns>
<hook_composition>
Multiple hooks for same event run in alphabetical order by filename.
Use numeric prefixes:
hooks/
├── 00-log-prompt.sh # First (logging)
├── 10-inject-context.sh # Second (context)
├── 20-activate-skills.sh # Third (skills)
└── 99-notify.sh # Last (notifications)
If Hook B depends on Hook A's output:
Example:
# 10-track-edits.sh writes to edit-log.txt
# 20-check-builds.sh reads from edit-log.txt
</hook_composition>
<testing_hooks>
# Manually trigger
bash ~/.claude/hooks/build-checker.sh
# Check exit code
echo $? # 0 = success
# Create mock log
echo "/path/to/test/file.ts" > /tmp/test-edit-log.txt
# Run with test data
EDIT_LOG=/tmp/test-edit-log.txt bash ~/.claude/hooks/build-checker.sh
Enable logging:
set -x # Debug output
exec 2>~/.claude/hooks/debug.log
Check execution:
tail -f ~/.claude/logs/hooks.log
Common issues:
# Developer frustrated by TypeScript errors
# Creates blocking Stop hook immediately:
#!/bin/bash
npm run build
if [ $? -ne 0 ]; then
echo "BUILD FAILED - BLOCKING"
exit 1 # Blocks Claude
fi
<why_it_fails>
#!/bin/bash
# Non-blocking observation
npm run build 2>&1 | tee /tmp/build.log
if grep -q "error TS" /tmp/build.log; then
echo "🔴 Build errors found (not blocking)"
fi
After 1 week, review:
Phase 2: If errors are frequent, automate
#!/bin/bash
# Still non-blocking, but more helpful
npm run build 2>&1 | tee /tmp/build.log
error_count=$(grep -c "error TS" /tmp/build.log || echo "0")
if [ "$error_count" -ge 5 ]; then
echo "⚠️ $error_count errors - consider using error-resolver agent"
elif [ "$error_count" -gt 0 ]; then
echo "🔴 $error_count errors (not blocking):"
grep "error TS" /tmp/build.log | head -5
fi
Phase 3: Only if observation shows blocking is necessary
Never reached - non-blocking works fine!
What you gain:
#!/bin/bash
# Stop hook that's too slow
Run full test suite (takes 45 seconds!)
npm test
Run linter (takes 10 seconds)
npm run lint
Run build (takes 30 seconds)
npm run build
Total: 85 seconds of blocking!
<why_it_fails>
#!/bin/bash
# Stop hook - fast checks only
# Quick syntax check (< 1 second)
npm run check-syntax
if [ $? -ne 0 ]; then
echo "🔴 Syntax errors found"
echo "💡 Run 'npm test' manually for full test suite"
fi
echo "✅ Quick checks passed (run 'npm test' for full suite)"
Or run slow checks in background:
#!/bin/bash
# Stop hook - trigger background job
# Start tests in background
(
npm test > /tmp/test-results.txt 2>&1
if [ $? -ne 0 ]; then
echo "🔴 Tests failed (see /tmp/test-results.txt)"
fi
) &
echo "⏳ Tests running in background (check /tmp/test-results.txt)"
What you gain:
#!/bin/bash
# Hook with no error handling
file=$(tail -1 ~/.claude/edit-log.txt)
prettier --write "$file"
<why_it_fails>
#!/bin/bash
set -euo pipefail # Exit on error, undefined vars
# Log execution
echo "[$(date)] Hook started" >> ~/.claude/hooks/formatter.log
# Validate input
if [ ! -f ~/.claude/edit-log.txt ]; then
echo "[$(date)] ERROR: edit-log.txt not found" >> ~/.claude/hooks/formatter.log
exit 1
fi
file=$(tail -1 ~/.claude/edit-log.txt | grep "^/.*\.ts$")
if [ -z "$file" ]; then
echo "[$(date)] No TypeScript file to format" >> ~/.claude/hooks/formatter.log
exit 0
fi
if [ ! -f "$file" ]; then
echo "[$(date)] ERROR: File not found: $file" >> ~/.claude/hooks/formatter.log
exit 1
fi
# Check prettier exists
if ! command -v prettier &> /dev/null; then
echo "[$(date)] ERROR: prettier not installed" >> ~/.claude/hooks/formatter.log
exit 1
fi
# Format
echo "[$(date)] Formatting: $file" >> ~/.claude/hooks/formatter.log
if prettier --write "$file" 2>&1 | tee -a ~/.claude/hooks/formatter.log; then
echo "✅ Formatted $file"
else
echo "🔴 Formatting failed (see ~/.claude/hooks/formatter.log)"
fi
What you gain:
❌ Don't:
# DANGEROUS - executes arbitrary code
cmd=$(tail -1 ~/.claude/edit-log.txt)
eval "$cmd"
✅ Do:
# SAFE - validates and sanitizes
file=$(tail -1 ~/.claude/edit-log.txt | grep "^/.*\.ts$")
if [ -f "$file" ]; then
prettier --write "$file"
fi
<critical_rules>
All of these mean: STOP. Follow progressive enhancement.
<verification_checklist> Before deploying hook:
Can't check all boxes? Return to development and fix. </verification_checklist>
**This skill covers:** Hook creation and patternsRelated skills:
Hook patterns support:
Official documentation:
When stuck:
npx claudepluginhub dpolishuk/xpowersConfigures Claude Code hooks for lifecycle events like PreToolUse, SessionStart, and automation use cases such as formatting enforcement and permission control.
Create event-driven hooks for Claude Code automation. Use when the user wants to create hooks, automate tool validation, add pre/post processing, enforce security policies, or configure settings.json hooks. Triggers: create hook, build hook, PreToolUse, PostToolUse, event automation, tool validation, security hook
Guides creation of Claude Code plugin hooks for event-driven automation: PreToolUse, PostToolUse, Stop, SessionStart, etc. Covers prompt-based and command hooks with plugin and user settings formats.