Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

33 KiB
Raw Permalink Blame History

Complete Guide to Claude Code Hooks

This guide teaches you how to create and configure custom hooks for Claude Code to automate workflows, validate operations, and enhance your development experience.

What are Claude Code Hooks?

Hooks are automated scripts that execute at specific points during Claude Code sessions. Each hook:

  • Responds to specific events like tool usage, prompt submission, or session start/end
  • Can validate, modify, or block operations based on your criteria
  • Executes shell commands automatically with access to session data
  • Integrates seamlessly with your development workflow
  • Provides fine-grained control over Claude's behavior

Key Benefits

🔄 Automated Workflows

Execute custom scripts automatically when specific events occur during Claude Code sessions.

🛡️ Enhanced Security

Validate and block potentially dangerous operations before they execute.

📊 Session Monitoring

Track usage patterns, log operations, and gather analytics from your Claude Code sessions.

🔧 Custom Validation

Implement project-specific rules and requirements that Claude must follow.

Real-time Feedback

Provide immediate feedback to Claude based on the results of operations.

Hook Configuration

Configuration Files

Hooks are configured in your Claude Code settings files with the following priority order:

Type Location Scope Usage
User Settings ~/.claude/settings.json All projects Personal hooks across all projects
Project Settings .claude/settings.json Current project Shared team hooks (committed to repo)
Local Project Settings .claude/settings.local.json Current project Personal project hooks (not committed)
Enterprise Settings Managed by policy Organization Company-wide hooks and policies

When there are conflicts, settings files lower in the list take precedence.

Basic Hook Structure

Each hook is defined using event-based configuration:

{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-script-here",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Configuration Elements

Event Names

  • PreToolUse: Before Claude executes a tool
  • PostToolUse: After Claude executes a tool successfully
  • UserPromptSubmit: When user submits a prompt
  • Notification: When Claude Code sends notifications
  • Stop: When Claude finishes responding
  • SubagentStop: When a subagent completes
  • SessionStart: When a session begins or resumes
  • SessionEnd: When a session terminates
  • PreCompact: Before compacting conversation history

Matchers

For tool-based events (PreToolUse, PostToolUse):

"matcher": "Write"              // Exact match for Write tool
"matcher": "Edit|Write"         // Regex pattern for multiple tools
"matcher": "Bash.*"            // Pattern matching with wildcards
"matcher": "*"                 // Match all tools
"matcher": ""                  // Also matches all tools

Hook Commands

{
  "type": "command",                           // Currently only "command" supported
  "command": "/path/to/script.sh",            // Shell command to execute
  "timeout": 30                               // Optional timeout in seconds (default: 60)
}

Hook Events Reference

PreToolUse

Purpose: Validate or modify tool usage before execution Common Use Cases: Permission control, input validation, security checks

Input Data:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  }
}

Common Matchers:

  • Bash - Shell commands
  • Write - File creation
  • Edit, MultiEdit - File modifications
  • Read - File reading
  • Task - Subagent tasks
  • WebFetch, WebSearch - Web operations

PostToolUse

Purpose: React to completed tool operations, provide feedback, or trigger follow-up actions Common Use Cases: Logging, validation, cleanup, triggering builds

Input Data:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  },
  "tool_response": {
    "filePath": "/path/to/file.txt",
    "success": true
  }
}

UserPromptSubmit

Purpose: Process or validate user prompts before Claude sees them Common Use Cases: Adding context, blocking sensitive prompts, prompt transformation

Input Data:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Write a function to calculate factorial"
}

SessionStart

Purpose: Initialize session context and load project-specific information Common Use Cases: Loading project state, setting up environment, adding context

Input Data:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "hook_event_name": "SessionStart",
  "source": "startup"  // "startup", "resume", or "clear"
}

Matchers:

  • startup - Normal Claude Code startup
  • resume - Resumed from --resume, --continue, or /resume
  • clear - After /clear command

Stop and SubagentStop

Purpose: Control whether Claude can finish responding or trigger additional actions Common Use Cases: Automated testing, continuous workflows, quality checks

Input Data:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}

SessionEnd

Purpose: Cleanup tasks when a session terminates Common Use Cases: Saving session data, cleanup, logging statistics

Input Data:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/conversation.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "SessionEnd",
  "reason": "exit"  // "clear", "logout", "prompt_input_exit", "other"
}

Hook Output and Control

Simple Exit Code Control

Exit Code 0: Success

  • For most hooks: stdout shown to user in transcript mode (Ctrl+R)
  • For UserPromptSubmit and SessionStart: stdout added as context for Claude

Exit Code 2: Blocking error

  • Blocks the operation and shows stderr to Claude for automated handling
  • Behavior varies by hook type (see reference table below)

Other Exit Codes: Non-blocking error

  • Shows stderr to user and continues execution

Exit Code 2 Behavior by Hook Type

Hook Event Behavior
PreToolUse Blocks tool execution, shows stderr to Claude
PostToolUse Shows stderr to Claude (tool already executed)
UserPromptSubmit Blocks prompt, erases it, shows stderr to user only
Stop/SubagentStop Blocks stopping, shows stderr to Claude
Notification Shows stderr to user only
SessionStart/SessionEnd/PreCompact Shows stderr to user only

Advanced JSON Output

For sophisticated control, hooks can return structured JSON:

Common JSON Fields

{
  "continue": true,                    // Whether to continue execution (default: true)
  "stopReason": "string",             // Message when continue is false
  "suppressOutput": true,             // Hide stdout from transcript (default: false)
  "systemMessage": "string"           // Warning message for user
}

PreToolUse Permission Control

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",     // "allow", "deny", or "ask"
    "permissionDecisionReason": "Auto-approved safe operation"
  }
}

PostToolUse Feedback Control

{
  "decision": "block",                // "block" or undefined
  "reason": "Code style violations detected",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Additional context for Claude"
  }
}

UserPromptSubmit Processing

{
  "decision": "block",                // "block" or undefined  
  "reason": "Sensitive information detected",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Current project status: Ready for deployment"
  }
}

SessionStart Context Loading

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart", 
    "additionalContext": "Recent issues: #123 (bug fix needed), #124 (feature ready)"
  }
}

Complete Hook Examples

1. Code Style Enforcement

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

format-code.sh:

#!/bin/bash
# Code formatting hook

set -e

# Read hook input
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')

# Only process code files
if [[ ! "$file_path" =~ \.(js|ts|jsx|tsx|py|go|rs)$ ]]; then
    exit 0
fi

echo "🔧 Formatting $file_path..."

# Format based on file type
case "$file_path" in
    *.js|*.jsx|*.ts|*.tsx)
        if command -v prettier >/dev/null 2>&1; then
            prettier --write "$file_path" 2>/dev/null || echo "❌ Prettier formatting failed"
        fi
        ;;
    *.py)
        if command -v black >/dev/null 2>&1; then
            black "$file_path" 2>/dev/null || echo "❌ Black formatting failed"
        fi
        ;;
    *.go)
        if command -v gofmt >/dev/null 2>&1; then
            gofmt -w "$file_path" 2>/dev/null || echo "❌ Go formatting failed"
        fi
        ;;
    *.rs)
        if command -v rustfmt >/dev/null 2>&1; then
            rustfmt "$file_path" 2>/dev/null || echo "❌ Rust formatting failed"
        fi
        ;;
esac

echo "✅ Code formatting complete"

2. Security Validation

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command", 
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-sensitive.py"
          }
        ]
      }
    ]
  }
}

validate-bash.py:

#!/usr/bin/env python3
import json
import sys
import re

# Dangerous command patterns
DANGEROUS_PATTERNS = [
    (r'\brm\s+-rf\s+/', 'Dangerous: rm -rf with absolute path'),
    (r'\bdd\s+if=', 'Dangerous: dd command detected'),
    (r'\bchmod\s+777', 'Security risk: chmod 777 detected'),
    (r'>\s*/dev/(sd[a-z]|hd[a-z])', 'Dangerous: writing to disk device'),
    (r'\bcurl.*\|\s*bash', 'Security risk: piping curl to bash'),
    (r'\bwget.*\|\s*bash', 'Security risk: piping wget to bash'),
]

try:
    input_data = json.load(sys.stdin)
    command = input_data.get('tool_input', {}).get('command', '')
    
    if not command:
        sys.exit(0)
    
    # Check for dangerous patterns
    for pattern, message in DANGEROUS_PATTERNS:
        if re.search(pattern, command, re.IGNORECASE):
            print(f"🚨 {message}", file=sys.stderr)
            print(f"Command: {command}", file=sys.stderr)
            print("Please review and confirm this command is safe.", file=sys.stderr)
            sys.exit(2)  # Block the command
    
    # Auto-approve safe commands
    output = {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "allow",
            "permissionDecisionReason": "Command passed security validation"
        }
    }
    print(json.dumps(output))
    
except Exception as e:
    print(f"Hook error: {e}", file=sys.stderr)
    sys.exit(1)

check-sensitive.py:

#!/usr/bin/env python3
import json
import sys
import re

# Sensitive information patterns
SENSITIVE_PATTERNS = [
    (r'(?i)(password|pwd)\s*[:=]\s*[\'"]?[^\s\'"]+', 'Password detected'),
    (r'(?i)(api[_-]?key|apikey)\s*[:=]\s*[\'"]?[a-zA-Z0-9]+', 'API key detected'),
    (r'(?i)(secret|token)\s*[:=]\s*[\'"]?[^\s\'"]+', 'Secret/token detected'),
    (r'\b[A-Za-z0-9+/]{40,}={0,2}\b', 'Base64-encoded secret detected'),
    (r'-----BEGIN [A-Z ]+-----', 'Private key detected'),
]

try:
    input_data = json.load(sys.stdin)
    prompt = input_data.get('prompt', '')
    
    # Check for sensitive information
    for pattern, message in SENSITIVE_PATTERNS:
        if re.search(pattern, prompt):
            output = {
                "decision": "block",
                "reason": f"🚨 Security violation: {message}. Please remove sensitive information and try again."
            }
            print(json.dumps(output))
            sys.exit(0)
    
    # Add security reminder
    security_context = """
🔒 Security reminder: This session is being monitored for sensitive information.
Current security status: All prompts are scanned for passwords, API keys, and secrets.
"""
    
    output = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": security_context
        }
    }
    print(json.dumps(output))
    
except Exception as e:
    print(f"Hook error: {e}", file=sys.stderr)
    sys.exit(1)

3. Project Context Loading

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/load-context.sh"
          }
        ]
      }
    ]
  }
}

load-context.sh:

#!/bin/bash
# Project context loading hook

set -e

# Read hook input
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id')
source=$(echo "$input" | jq -r '.source // "startup"')

echo "🚀 Loading project context for $source..."

# Load git status
echo "## Git Repository Status"
echo "**Current branch:** $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'not a git repo')"
echo "**Last commit:** $(git log -1 --pretty=format:'%h - %s (%cr)' 2>/dev/null || echo 'no commits')"

# Check for uncommitted changes
if git diff-index --quiet HEAD -- 2>/dev/null; then
    echo "**Working tree:** clean"
else
    echo "**Working tree:** has uncommitted changes"
    echo "**Modified files:**"
    git diff --name-only HEAD 2>/dev/null | sed 's/^/  - /' || echo "  - (unable to detect)"
fi

# Load recent issues/todos
echo -e "\n## Recent Issues & TODOs"
if [[ -f "TODO.md" ]]; then
    echo "**From TODO.md:**"
    head -10 "TODO.md" | sed 's/^/  /'
elif command -v rg >/dev/null 2>&1; then
    echo "**TODOs in codebase:**"
    rg -i "todo|fixme|hack|bug" --type-add 'code:*.{js,ts,py,go,rs,java,c,cpp,h}' -t code -n | head -5 | sed 's/^/  /'
fi

# Load package info
echo -e "\n## Project Configuration"
if [[ -f "package.json" ]]; then
    name=$(jq -r '.name // "unknown"' package.json)
    version=$(jq -r '.version // "unknown"' package.json)
    echo "**Project:** $name v$version"
    echo "**Scripts available:** $(jq -r '.scripts | keys | join(", ")' package.json 2>/dev/null || echo 'none')"
elif [[ -f "Cargo.toml" ]]; then
    name=$(grep '^name' Cargo.toml | cut -d'"' -f2 2>/dev/null || echo 'unknown')
    version=$(grep '^version' Cargo.toml | cut -d'"' -f2 2>/dev/null || echo 'unknown')
    echo "**Rust project:** $name v$version"
elif [[ -f "go.mod" ]]; then
    module=$(head -1 go.mod | cut -d' ' -f2 2>/dev/null || echo 'unknown')
    echo "**Go module:** $module"
elif [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
    echo "**Python project detected**"
fi

# Load recent changes
echo -e "\n## Recent Activity"
echo "**Last 3 commits:**"
git log -3 --pretty=format:'  - %h %s (%cr)' 2>/dev/null || echo "  - No git history available"

echo -e "\n✅ Project context loaded successfully"

4. Automated Testing

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-test.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

auto-test.sh:

#!/bin/bash
# Automated testing hook

set -e

# Read hook input
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')

# Only run tests for source files
if [[ ! "$file_path" =~ \.(js|ts|jsx|tsx|py|go|rs)$ ]]; then
    exit 0
fi

echo "🧪 Running automated tests for $file_path..."

# Determine test command based on project type
run_tests() {
    if [[ -f "package.json" ]] && jq -e '.scripts.test' package.json >/dev/null 2>&1; then
        echo "📦 Running npm tests..."
        npm test 2>&1 | tail -10
    elif [[ -f "Cargo.toml" ]]; then
        echo "🦀 Running Rust tests..."
        cargo test 2>&1 | tail -10
    elif [[ -f "go.mod" ]]; then
        echo "🐹 Running Go tests..."
        go test ./... 2>&1 | tail -10
    elif [[ -f "pytest.ini" ]] || [[ -f "pyproject.toml" ]]; then
        echo "🐍 Running Python tests..."
        python -m pytest -x --tb=short 2>&1 | tail -10
    else
        echo "️  No test configuration found, skipping tests"
        return 0
    fi
}

# Run tests and capture result
if test_output=$(run_tests); then
    echo "✅ Tests passed"
    echo "$test_output"
else
    echo "❌ Tests failed - informing Claude"
    echo "Test failures detected in modified code:" >&2
    echo "$test_output" >&2
    exit 2  # This will show the error to Claude
fi

5. File Protection

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.py"
          }
        ]
      }
    ]
  }
}

protect-files.py:

#!/usr/bin/env python3
import json
import sys
import os
from pathlib import Path

# Protected file patterns
PROTECTED_PATTERNS = [
    '.env*',
    '*.key',
    '*.pem', 
    'id_rsa*',
    'id_ed25519*',
    '.aws/credentials',
    '.ssh/config',
    'secrets.*',
    'private.*'
]

# Critical system files
CRITICAL_FILES = [
    '/etc/passwd',
    '/etc/shadow', 
    '/etc/hosts',
    '/etc/fstab',
    '~/.bashrc',
    '~/.zshrc',
    '~/.profile'
]

def is_protected_file(file_path):
    """Check if file matches protection patterns"""
    path = Path(file_path)
    
    # Check against patterns
    for pattern in PROTECTED_PATTERNS:
        if path.match(pattern) or any(part.match(pattern) for part in path.parts):
            return True, f"matches protected pattern: {pattern}"
    
    # Check critical system files
    resolved_path = str(path.resolve())
    for critical in CRITICAL_FILES:
        critical_resolved = str(Path(critical).expanduser().resolve())
        if resolved_path == critical_resolved:
            return True, f"is a critical system file"
    
    return False, None

try:
    input_data = json.load(sys.stdin)
    tool_input = input_data.get('tool_input', {})
    file_path = tool_input.get('file_path', '')
    
    if not file_path:
        sys.exit(0)
    
    # Check if file is protected
    is_protected, reason = is_protected_file(file_path)
    
    if is_protected:
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "ask",
                "permissionDecisionReason": f"⚠️ Protected file detected: {file_path} {reason}. Confirm this operation."
            }
        }
        print(json.dumps(output))
    else:
        # Auto-approve safe files
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse", 
                "permissionDecision": "allow",
                "permissionDecisionReason": "File passed protection checks"
            }
        }
        print(json.dumps(output))
        
except Exception as e:
    print(f"Hook error: {e}", file=sys.stderr)
    sys.exit(1)

MCP Integration

Understanding MCP Tools in Hooks

Claude Code hooks work seamlessly with Model Context Protocol (MCP) tools. MCP tools follow the naming pattern mcp__<server>__<tool>:

  • mcp__github__create_issue - GitHub MCP server's issue creation tool
  • mcp__database__execute_query - Database MCP server's query tool
  • mcp__filesystem__write_file - Filesystem MCP server's write tool

MCP Hook Configuration

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__github__.*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-github-operations.sh"
          }
        ]
      },
      {
        "matcher": "mcp__.*__write.*|mcp__.*__delete.*",
        "hooks": [
          {
            "type": "command", 
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-destructive-mcp.py"
          }
        ]
      }
    ]
  }
}

Advanced Hook Patterns

1. Conditional Hook Execution

#!/usr/bin/env python3
# Conditional hook based on project type and environment

import json
import sys
import os

try:
    input_data = json.load(sys.stdin)
    
    # Only run in production-like environments
    env = os.environ.get('NODE_ENV', 'development')
    if env not in ['staging', 'production']:
        sys.exit(0)
    
    # Only for specific project types
    if not os.path.exists('package.json'):
        sys.exit(0)
    
    # Your hook logic here
    print("🚀 Production hook executed")
    
except Exception as e:
    print(f"Hook error: {e}", file=sys.stderr)
    sys.exit(1)

2. Multi-Tool Coordination

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/coordinate-tools.sh"
          }
        ]
      }
    ]
  }
}

coordinate-tools.sh:

#!/bin/bash
# Multi-tool coordination example

set -e

# Read hook input
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
tool_name=$(echo "$input" | jq -r '.tool_name')

# Create a coordination log
mkdir -p .claude/logs
echo "$(date): $tool_name - $file_path" >> .claude/logs/tool-coordination.log

# Trigger related actions based on file type
case "$file_path" in
    *.md)
        echo "📝 Documentation updated, checking for broken links..."
        # Link check could go here
        ;;
    *.js|*.ts)
        echo "📄 JavaScript updated, checking dependencies..."
        if [[ -f package.json ]]; then
            npm audit --audit-level=moderate || echo "⚠️ npm audit found issues"
        fi
        ;;
    Dockerfile)
        echo "🐳 Dockerfile updated, validating..."
        docker build --dry-run . || echo "⚠️ Docker build validation failed"
        ;;
    *.yml|*.yaml)
        echo "📋 YAML updated, validating syntax..."
        python -c "import yaml; yaml.safe_load(open('$file_path'))" 2>/dev/null || echo "⚠️ YAML syntax error"
        ;;
esac

echo "✅ Tool coordination complete"

3. Session Analytics

#!/usr/bin/env python3
# Session analytics and usage tracking

import json
import sys
import sqlite3
from datetime import datetime
import os

DATABASE = os.path.expanduser('~/.claude/analytics.db')

def init_db():
    """Initialize analytics database"""
    conn = sqlite3.connect(DATABASE)
    conn.execute('''
        CREATE TABLE IF NOT EXISTS tool_usage (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id TEXT,
            tool_name TEXT,
            timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
            file_path TEXT,
            success BOOLEAN
        )
    ''')
    conn.commit()
    return conn

try:
    input_data = json.load(sys.stdin)
    
    # Initialize database
    conn = init_db()
    
    # Log tool usage
    conn.execute('''
        INSERT INTO tool_usage (session_id, tool_name, file_path, success)
        VALUES (?, ?, ?, ?)
    ''', (
        input_data.get('session_id'),
        input_data.get('tool_name'),
        input_data.get('tool_input', {}).get('file_path', ''),
        input_data.get('tool_response', {}).get('success', True)
    ))
    
    conn.commit()
    
    # Generate usage summary periodically
    cursor = conn.cursor()
    cursor.execute('SELECT COUNT(*) FROM tool_usage WHERE date(timestamp) = date("now")')
    daily_count = cursor.fetchone()[0]
    
    if daily_count % 10 == 0:  # Every 10th operation
        print(f"📊 Daily tool usage: {daily_count} operations")
        
        # Show top tools today
        cursor.execute('''
            SELECT tool_name, COUNT(*) as count
            FROM tool_usage 
            WHERE date(timestamp) = date("now")
            GROUP BY tool_name
            ORDER BY count DESC
            LIMIT 3
        ''')
        
        top_tools = cursor.fetchall()
        print("🏆 Most used tools today:")
        for tool, count in top_tools:
            print(f"  {tool}: {count} times")
    
    conn.close()
    
except Exception as e:
    print(f"Analytics error: {e}", file=sys.stderr)
    sys.exit(0)  # Don't fail the operation for analytics errors

Best Practices

1. Hook Design Principles

  • Single Responsibility: Each hook should have one clear purpose
  • Fail Gracefully: Use exit code 0 for non-critical failures to avoid blocking operations
  • Be Fast: Keep hook execution time minimal to avoid slowing down Claude
  • Idempotent: Hooks should be safe to run multiple times with the same input

2. Error Handling

#!/usr/bin/env python3
import json
import sys
import logging

# Set up logging
logging.basicConfig(
    filename=os.path.expanduser('~/.claude/hooks.log'),
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

try:
    input_data = json.load(sys.stdin)
    
    # Your hook logic here
    
except json.JSONDecodeError as e:
    logging.error(f"JSON decode error: {e}")
    print(f"Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)
except Exception as e:
    logging.error(f"Unexpected error: {e}")
    print(f"Hook error: {e}", file=sys.stderr)
    sys.exit(0)  # Don't block on unexpected errors

3. Input Validation

def validate_input(input_data):
    """Validate hook input data"""
    required_fields = ['session_id', 'hook_event_name']
    
    for field in required_fields:
        if field not in input_data:
            raise ValueError(f"Missing required field: {field}")
    
    # Validate file paths for path traversal
    file_path = input_data.get('tool_input', {}).get('file_path')
    if file_path and '..' in file_path:
        raise ValueError(f"Invalid file path: {file_path}")
    
    return True

4. Configuration Management

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/configurable-hook.py --config=$CLAUDE_PROJECT_DIR/.claude/hook-config.json"
          }
        ]
      }
    ]
  }
}

Security and Safety

Security Best Practices

  1. Input Sanitization: Always validate and sanitize inputs from Claude Code
  2. Path Security: Check for directory traversal attacks (../ patterns)
  3. Command Injection: Never pass user input directly to shell commands
  4. File Permissions: Use minimal required permissions for hook scripts
  5. Secrets Management: Never log or expose sensitive information

Security Example

#!/usr/bin/env python3
import json
import sys
import os
import re
from pathlib import Path

def sanitize_path(file_path):
    """Sanitize file path to prevent directory traversal"""
    if not file_path:
        return None
    
    # Check for directory traversal
    if '..' in file_path or file_path.startswith('/'):
        raise ValueError(f"Unsafe path detected: {file_path}")
    
    # Resolve to absolute path within project
    project_dir = os.environ.get('CLAUDE_PROJECT_DIR', '.')
    safe_path = Path(project_dir) / file_path
    
    # Ensure path stays within project directory
    try:
        safe_path.resolve().relative_to(Path(project_dir).resolve())
        return str(safe_path)
    except ValueError:
        raise ValueError(f"Path outside project directory: {file_path}")

try:
    input_data = json.load(sys.stdin)
    file_path = input_data.get('tool_input', {}).get('file_path')
    
    if file_path:
        safe_path = sanitize_path(file_path)
        print(f"✅ Safe path validated: {safe_path}")
    
except Exception as e:
    print(f"Security validation failed: {e}", file=sys.stderr)
    sys.exit(2)  # Block unsafe operations

Hook Execution Safety

  • Timeout Limits: Always set reasonable timeouts to prevent hanging
  • Resource Limits: Be mindful of CPU and memory usage
  • Concurrent Execution: Hooks run in parallel - design for thread safety
  • Environment Isolation: Use containers or sandboxing for untrusted hooks

Troubleshooting and Debugging

Common Issues

  1. Hook Not Executing

    • Check hook registration with /hooks command
    • Verify JSON syntax in settings files
    • Ensure script is executable (chmod +x script.sh)
    • Check matcher patterns are correct (case-sensitive)
  2. Permission Errors

    • Verify script file permissions
    • Check $CLAUDE_PROJECT_DIR environment variable
    • Ensure scripts use absolute paths
  3. Timeout Issues

    • Increase timeout values for long-running operations
    • Optimize hook performance
    • Run expensive operations asynchronously

Debug Mode

Use claude --debug to see detailed hook execution:

claude --debug

Debug output includes:

  • Hook discovery and matching
  • Command execution details
  • Execution time and exit codes
  • stdout/stderr from hooks

Testing Hooks

Test hooks independently:

# Test hook script directly
echo '{"session_id":"test","hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"test.txt"}}' | ./.claude/hooks/your-hook.py

# Test JSON output
echo '{"session_id":"test"}' | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(json.dumps({'continue': True, 'systemMessage': 'Test successful'}))
"

Hook Development Workflow

  1. Start Simple: Begin with basic exit code hooks
  2. Add Logging: Include debug output for troubleshooting
  3. Test Thoroughly: Test all code paths and edge cases
  4. Add Error Handling: Graceful degradation on failures
  5. Optimize Performance: Minimize execution time
  6. Document Behavior: Clear comments and documentation

Hook Ecosystem

Community Hooks

Share and discover hooks with the community:

Hook Templates

Use these templates as starting points:

Basic Hook Template:

#!/usr/bin/env python3
import json
import sys
import os

def main():
    try:
        # Read input
        input_data = json.load(sys.stdin)
        
        # Your hook logic here
        
        # Success
        print("Hook executed successfully")
        sys.exit(0)
        
    except Exception as e:
        print(f"Hook error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

JSON Output Template:

#!/usr/bin/env python3
import json
import sys

def main():
    try:
        input_data = json.load(sys.stdin)
        
        # Your logic here
        
        output = {
            "continue": True,
            "hookSpecificOutput": {
                "hookEventName": input_data.get('hook_event_name'),
                # Event-specific output
            }
        }
        
        print(json.dumps(output))
        sys.exit(0)
        
    except Exception as e:
        print(f"Hook error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

Conclusion

Claude Code hooks provide powerful automation capabilities that can significantly enhance your development workflow. With hooks, you can:

  • Automate repetitive tasks like code formatting and testing
  • Enhance security through validation and monitoring
  • Customize Claude's behavior to match your team's requirements
  • Integrate with external tools and services
  • Monitor and analyze your development patterns

Start with simple hooks and gradually build more sophisticated automation as you become comfortable with the system. Remember to prioritize security, test thoroughly, and document your hooks for team collaboration.

For more examples and community resources, see the Claude Code documentation and join the community discussions.