Files
2025-10-28 12:48:17 -05:00

7.3 KiB

name, description
name description
Claude Code Hooks Configure event-driven hooks for Claude Code to automate workflows, validate code, inject context, and control tool execution. Use PROACTIVELY when users want automation that runs automatically (not manually), mention "automatically run", "on save", "after editing", "event-driven", "workflow automation", or "enforce rules". NOT for one-time scripts.

Claude Code Hooks

When to Use This Skill

Use this skill when:

  • Automating workflows with event triggers
  • Enforcing code standards or policies
  • Validating changes before/after tool execution
  • Injecting context at session start
  • Logging or monitoring tool usage
  • Setting up team-wide automation

Do NOT use this skill for:

  • Creating slash commands (use claude-commands skill)
  • Building plugins (use claude-plugins skill)
  • One-time scripts (run them directly)

Quick Start

Configuration File Locations

.claude/settings.json          # Project (team)
.claude/settings.local.json    # Project (personal)
~/.claude/settings.json        # Global (personal)

Simple Example - Auto-Format

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "npx prettier --write $TOOL_ARGS && exit 0"
      }]
    }]
  }
}

Hook Types

Event When Common Use
SessionStart Session begins Inject context
SessionEnd Session ends Cleanup, backups
PreToolUse Before tool execution Validation, checks
PostToolUse After tool completes Formatting, linting
UserPromptSubmit User submits prompt Logging
Notification Claude sends notification Desktop alerts
Stop Agent finishes Post-processing

Core Concepts

Matchers

// Single tool
"matcher": "Write"

// Multiple (OR)
"matcher": "Write|Edit|Read"

// All tools
"matcher": "*"

// Git operations
"matcher": "Bash(git:*)"

Exit Codes

exit 0    # Success - Continue
exit 1    # Non-blocking error - Continue
exit 2    # Blocking error - Stop operation

Environment Variables

$CLAUDE_PROJECT_DIR   # Current project
$TOOL_NAME           # Tool being used
$TOOL_ARGS           # Tool arguments
$HOOK_EVENT          # Event type

Common Use Cases

Auto-Format on Save

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "cd $CLAUDE_PROJECT_DIR && npx prettier --write $TOOL_ARGS && exit 0"
      }]
    }]
  }
}

Security Validation

{
  "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' >&2; exit 2; fi; exit 0",
        "timeout": 30
      }]
    }]
  }
}

Test Before Commit

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "cd $CLAUDE_PROJECT_DIR && npm test -- --silent || (echo 'Tests failed' >&2; exit 2)",
        "timeout": 120
      }]
    }]
  }
}

For more examples, see examples.md

Hook Configuration

Basic Structure

{
  "type": "command",
  "command": "your-command-here",
  "timeout": 60  // Optional, default 60 seconds
}

Multiple Hooks (Sequential)

{
  "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"}
      ]
    }]
  }
}

Conditional Execution

{
  "type": "command",
  "command": "if [[ $TOOL_ARGS == *.js ]]; then npm run format $TOOL_ARGS; fi; exit 0"
}

Settings Hierarchy

Load Order (highest to lowest priority):

  1. .claude/settings.local.json - Project, personal (gitignored)
  2. .claude/settings.json - Project, team (in git)
  3. ~/.claude/settings.json - Global, personal

Use Cases:

  • Global: Personal preferences, universal logging
  • Project: Team standards, project automation
  • Local: Personal overrides, experiments

Testing Hooks

Start Simple

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "echo 'Hook triggered: $TOOL_ARGS' && exit 0"
      }]
    }]
  }
}

Test Exit Codes

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write",
      "hooks": [{
        "type": "command",
        "command": "echo 'This blocks' >&2 && exit 2"
      }]
    }]
  }
}

Try writing - should be blocked.

Best Practices

1. Always Exit Explicitly

# ✓ Good
command && exit 0

# ✗ Bad
command

2. Use Timeouts

{
  "command": "npm test",
  "timeout": 120  // Don't hang forever
}

3. Handle Errors Gracefully

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

// ✓ Good - external script
{
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check.sh $TOOL_ARGS"
}

// ✗ Bad - complex bash in JSON
{
  "command": "if [ -f $TOOL_ARGS ]; then cat $TOOL_ARGS | grep pattern | wc -l; fi"
}

5. Test Before Team Deployment

Test in .claude/settings.local.json before adding to team settings.

Troubleshooting

Problem Solution
Not triggering Check matcher, restart Claude
Fails silently Check exit codes (use 0 for success)
Command not found Use full path
Permission denied chmod +x script.sh
Timeout Increase timeout or optimize

For detailed troubleshooting, see troubleshooting.md

Security

Validate Input

# Sanitize TOOL_ARGS
if [[ ! $TOOL_ARGS =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
    echo "Invalid path" >&2
    exit 2
fi

Limit Permissions

// ✓ Specific
"matcher": "Write|Edit"

// ✗ Too broad
"matcher": "*"

Avoid Destructive Commands

# ✗ Dangerous
rm -rf $TOOL_ARGS

# ✓ Safer
if [[ -f "$TOOL_ARGS" ]] && [[ "$TOOL_ARGS" != "/" ]]; then
    rm "$TOOL_ARGS"
fi

Quick Reference

Common Patterns

// Format on save
"PostToolUse": [{"matcher": "Write|Edit", "hooks": [{"type": "command", "command": "prettier --write $TOOL_ARGS"}]}]

// Block secrets
"PreToolUse": [{"matcher": "Write", "hooks": [{"type": "command", "command": "grep -q 'secret' $TOOL_ARGS && exit 2 || exit 0"}]}]

// Log activity
"PreToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "echo \"$TOOL_NAME: $TOOL_ARGS\" >> .claude/log && exit 0"}]}]

Additional Resources

Need more?

💡 Tip: Start simple, test thoroughly, use exit codes correctly. Hooks are powerful - use them wisely.


Remember: Hooks automate workflows. Exit codes control flow. Test in local settings first. Use specific matchers for security.