--- name: Claude Code Hooks description: Configure event-driven hooks for Claude Code to automate workflows, validate code, inject context, and control tool execution. Use when setting up automation, enforcing standards, or integrating external tools with Claude Code. --- # Claude Code Hooks ## When to Use This Skill Use this skill when: - Automating workflows with event-driven triggers - Enforcing code standards or security policies - Validating changes before/after tool execution - Injecting context at session start - Logging or monitoring tool usage - Controlling MCP tool permissions - Setting up team-wide automation Do NOT use this skill for: - Creating slash commands (use claude-code-slash-commands skill) - Building full plugins (use claude-code-plugins skill) - One-time scripts (just run them directly) ## Quick Start: Creating a Hook ### 1. Basic Hook Configuration Create or edit settings file: ```bash # Project-wide (team shared) .claude/settings.json # User-specific (not shared) .claude/settings.local.json # Global (all projects) ~/.claude/settings.json ``` ### 2. Simple Example - Log File Changes ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "echo \"$(date): Modified $TOOL_ARGS\" >> .claude/changes.log" } ] } ] } } ``` **Result**: Every file write/edit gets logged with timestamp. ## Hook Types ### SessionStart Runs when a session begins (before any interaction): ```json { "hooks": { "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "echo 'Project: MyApp | Env: production' > .claude/context.txt" } ] } ] } } ``` **Use For**: Injecting project context, loading environment info ### SessionEnd Runs when a session ends: ```json { "hooks": { "SessionEnd": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "git add . && git stash save 'Claude session backup'" } ] } ] } } ``` **Use For**: Cleanup, backups, state preservation ### PreToolUse Runs after Claude creates tool parameters but before execution: ```json { "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "echo 'About to write: $TOOL_ARGS' && exit 0" } ] } ] } } ``` **Use For**: Validation, permission checks, preprocessing ### PostToolUse Runs immediately after tool completes successfully: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx prettier --write $TOOL_ARGS" } ] } ] } } ``` **Use For**: Formatting, linting, post-processing ### UserPromptSubmit Runs when user submits a prompt (before Claude processes it): ```json { "hooks": { "UserPromptSubmit": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "date >> .claude/prompt-log.txt && exit 0" } ] } ] } } ``` **Use For**: Logging, analytics, context injection ### Notification Runs when Claude sends notifications (permission requests, idle waiting): ```json { "hooks": { "Notification": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\"'" } ] } ] } } ``` **Use For**: Desktop notifications, alerts ### Stop / SubagentStop Runs when agents finish responding: ```json { "hooks": { "Stop": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "echo 'Response complete' >> .claude/activity.log && exit 0" } ] } ] } } ``` **Use For**: Post-response processing, logging ### PreCompact Runs before context compaction: ```json { "hooks": { "PreCompact": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "echo 'Compacting context...' && exit 0" } ] } ] } } ``` **Use For**: Logging, saving state before compaction ## Matcher Patterns ### Tool Name Matching ```json // Single tool "matcher": "Write" // Multiple tools (OR) "matcher": "Write|Edit|Read" // All tools "matcher": "*" // Regex patterns supported "matcher": "Bash.*|Git.*" ``` ### Common Patterns ```json // File operations "matcher": "Write|Edit|Delete" // Read operations "matcher": "Read|Grep|Glob" // Git operations "matcher": "Bash(git:*)" // All Bash commands "matcher": "Bash" ``` ## Hook Configuration ### Basic Structure ```json { "type": "command", "command": "your-command-here", "timeout": 60 } ``` ### Command Field Use bash commands with environment variables: ```json { "type": "command", "command": "cd $CLAUDE_PROJECT_DIR && npm test" } ``` ### Timeout Field Optional execution limit in seconds (default: 60): ```json { "type": "command", "command": "long-running-task", "timeout": 300 // 5 minutes } ``` ## Exit Codes & Control Flow ### Exit Code 0 - Success (Continue) ```bash # Hook succeeds, stdout shown to user (most events) echo "Success message" exit 0 ``` ### Exit Code 2 - Blocking Error ```bash # Hook blocks operation, stderr sent to Claude echo "Error: Cannot proceed" >&2 exit 2 ``` **Use For**: Validation failures, permission denials ### Other Exit Codes - Non-Blocking Error ```bash # Hook fails but operation continues echo "Warning: Hook failed" >&2 exit 1 ``` ## Environment Variables ### Available Variables ```bash $CLAUDE_PROJECT_DIR # Current project directory $TOOL_NAME # Name of the tool being used $TOOL_ARGS # Arguments passed to the tool $HOOK_EVENT # Event type (PreToolUse, PostToolUse, etc.) ``` ### Example Usage ```json { "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "cd $CLAUDE_PROJECT_DIR && git add $TOOL_ARGS" } ] } ] } } ``` ## Complete Examples ### 1. Auto-Format on Save Format files automatically after writing/editing: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "cd $CLAUDE_PROJECT_DIR && npx prettier --write $TOOL_ARGS && exit 0" } ] } ] } } ``` ### 2. Security Validation Block writes containing secrets: ```json { "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "if grep -qiE '(password|api[_-]?key|secret)' $TOOL_ARGS 2>/dev/null; then echo 'Error: Possible secret detected' >&2; exit 2; fi; exit 0", "timeout": 30 } ] } ] } } ``` ### 3. Auto-Git on Changes Automatically stage and commit changes: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "cd $CLAUDE_PROJECT_DIR && git add $TOOL_ARGS && git commit -m 'Auto-commit: Modified $TOOL_ARGS' && exit 0" } ] } ] } } ``` ### 4. Inject Project Context Load project info at session start: ```json { "hooks": { "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "cat << EOF\nProject: MyApp\nEnvironment: Production\nKey Files: src/config.js, .env.example\nCoding Standards: See CONTRIBUTING.md\nEOF" } ] } ] } } ``` ### 5. Test Before Commit Run tests before allowing file writes: ```json { "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "cd $CLAUDE_PROJECT_DIR && npm test -- --silent || (echo 'Tests failed, cannot save' >&2; exit 2)", "timeout": 120 } ] } ] } } ``` ### 6. Log All Activity Track all tool usage: ```json { "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') - $TOOL_NAME - $TOOL_ARGS\" >> $CLAUDE_PROJECT_DIR/.claude/activity.log && exit 0" } ] } ] } } ``` ### 7. Enforce File Permissions Ensure proper permissions on sensitive files: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "if [[ $TOOL_ARGS == *\".env\"* ]]; then chmod 600 $TOOL_ARGS; fi; exit 0" } ] } ] } } ``` ### 8. Notify on Long Sessions Alert when session ends: ```json { "hooks": { "SessionEnd": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "osascript -e 'display notification \"Session ended\" with title \"Claude Code\"' && exit 0" } ] } ] } } ``` ## Advanced Patterns ### Multiple Hooks per Event Run several hooks in sequence: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx prettier --write $TOOL_ARGS" }, { "type": "command", "command": "npx eslint --fix $TOOL_ARGS" }, { "type": "command", "command": "git add $TOOL_ARGS" } ] } ] } } ``` Hooks run in order; if one fails, subsequent hooks may not run. ### Conditional Execution Use bash conditionals: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "if [[ $TOOL_ARGS == *.js ]]; then npm run format $TOOL_ARGS; fi; exit 0" } ] } ] } } ``` ### Script-Based Hooks Call external scripts for complex logic: ```json { "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-write.sh $TOOL_ARGS", "timeout": 60 } ] } ] } } ``` **validate-write.sh**: ```bash #!/bin/bash file=$1 # Check file size if [ -f "$file" ] && [ $(wc -c < "$file") -gt 1000000 ]; then echo "Error: File too large" >&2 exit 2 fi # Check for secrets if grep -qiE '(password|api[_-]?key)' "$file" 2>/dev/null; then echo "Error: Possible secret detected" >&2 exit 2 fi exit 0 ``` ### JSON Output for Advanced Control Return structured data (advanced): ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo '{\"continue\": true, \"decision\": \"allow\"}'" } ] } ] } } ``` ## Settings File Hierarchy ### Load Order (Highest to Lowest Priority) 1. `.claude/settings.local.json` - Project-specific, user-only (gitignored) 2. `.claude/settings.json` - Project-specific, team-shared (in git) 3. `~/.claude/settings.json` - Global, user-only ### Use Cases **Global (~/.claude/settings.json)**: - Personal preferences - Universal logging - Desktop notifications **Project (.claude/settings.json)**: - Team standards - Project-specific automation - Shared workflows **Local (.claude/settings.local.json)**: - Personal overrides - Development experiments - User-specific credentials ## Testing Hooks ### 1. Start Simple Test with basic echo: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "echo 'Hook triggered for: $TOOL_ARGS' && exit 0" } ] } ] } } ``` ### 2. Verify Environment Variables ```json { "hooks": { "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "echo \"Project: $CLAUDE_PROJECT_DIR\" && exit 0" } ] } ] } } ``` ### 3. Test Exit Codes ```json { "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "echo 'This should block' >&2 && exit 2" } ] } ] } } ``` Try writing a file - should be blocked. ### 4. Check Timeout ```json { "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "sleep 5 && echo 'Done'", "timeout": 3 } ] } ] } } ``` Should timeout after 3 seconds. ## Common Use Cases ### Code Quality Enforcement ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx prettier --write $TOOL_ARGS && npx eslint --fix $TOOL_ARGS && exit 0" } ] } ] } } ``` ### Security Scanning ```json { "hooks": { "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "if grep -qE '(sk-[a-zA-Z0-9]{48}|ghp_[a-zA-Z0-9]{36})' $TOOL_ARGS 2>/dev/null; then echo 'API key detected!' >&2; exit 2; fi; exit 0" } ] } ] } } ``` ### Automatic Documentation ```json { "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "if [[ $TOOL_ARGS == *.js ]]; then npx jsdoc2md $TOOL_ARGS >> API.md; fi; exit 0" } ] } ] } } ``` ### Change Tracking ```json { "hooks": { "PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "echo \"$(date): $TOOL_NAME $TOOL_ARGS\" >> .claude/audit.log && exit 0" } ] } ] } } ``` ## Troubleshooting ### Hook Not Triggering **Issue**: Hook doesn't run when expected **Solutions**: 1. Check matcher pattern matches tool name 2. Verify settings file location and syntax (valid JSON) 3. Restart Claude Code to reload settings 4. Test with simple echo command first ### Hook Fails Silently **Issue**: Hook runs but doesn't work as expected **Solutions**: 1. Check exit codes (0 for success) 2. Verify command syntax in terminal first 3. Check timeout isn't too short 4. Look at stderr output ### Command Not Found **Issue**: Hook says command not found **Solutions**: 1. Use full path to command: `/usr/local/bin/prettier` 2. Check command is in PATH 3. Test command in terminal first ### Permission Denied **Issue**: Hook can't execute script **Solutions**: ```bash chmod +x .claude/hooks/script.sh ``` ## Best Practices ### 1. Always Exit with Code ```bash # Good - explicit exit command && exit 0 # Bad - implicit exit might vary command ``` ### 2. Use Timeouts for Long Operations ```json { "type": "command", "command": "npm test", "timeout": 120 // Don't let tests run forever } ``` ### 3. Handle Errors Gracefully ```bash # Check if file exists before processing if [ -f "$TOOL_ARGS" ]; then process_file "$TOOL_ARGS" else echo "File not found" >&2 fi exit 0 # Don't block on missing file ``` ### 4. Use Scripts for Complex Logic Don't put complex bash in JSON: ```json // Bad { "command": "if [ -f $TOOL_ARGS ]; then cat $TOOL_ARGS | grep pattern | wc -l; fi && exit 0" } // Good { "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-file.sh $TOOL_ARGS" } ``` ### 5. Test Before Deploying Test in `.claude/settings.local.json` before adding to `.claude/settings.json`. ### 6. Document Your Hooks Add comments in settings: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx prettier --write $TOOL_ARGS", "_comment": "Auto-format all JS/TS files on save" } ] } ] } } ``` (Note: JSON doesn't officially support comments, but Claude Code ignores `_comment` fields) ## Security Considerations ### Validate Input ```bash # Sanitize TOOL_ARGS before use if [[ ! $TOOL_ARGS =~ ^[a-zA-Z0-9/_.-]+$ ]]; then echo "Invalid file path" >&2 exit 2 fi ``` ### Limit Permissions Don't use `*` matcher unless necessary: ```json // Specific "matcher": "Write|Edit" // Too broad "matcher": "*" ``` ### Avoid Destructive Commands ```bash # Dangerous rm -rf $TOOL_ARGS # Better - add safety checks if [[ -f "$TOOL_ARGS" ]] && [[ "$TOOL_ARGS" != "/" ]]; then rm "$TOOL_ARGS" fi ``` ### Use Local Settings for Sensitive Operations Keep potentially dangerous hooks in `.claude/settings.local.json` (not shared). ## Resources - [Official Hooks Documentation](https://docs.claude.com/en/docs/claude-code/hooks) - [Slash Commands](../claude-code-slash-commands/SKILL.md) - For command-based automation - [Plugin Development](../claude-code-plugins/SKILL.md) - For packaging hooks in plugins --- **Remember**: Hooks are powerful automation tools. Start with simple, safe operations. Test thoroughly before deploying to teams. Use exit codes properly to control flow.