playbook/outfitter-agents/plugins/outfitter/skills/claude-hooks/references/examples.md

37 KiB

Hook Examples

Real-world examples of Claude Code event hooks for automation, validation, and workflow enhancement.

Table of Contents

  1. Auto-Formatting
  2. Validation Hooks
  3. CI/CD Integration
  4. Notification Systems
  5. Context Injection
  6. Security Enforcement
  7. Multi-Hook Workflows
  8. Team Collaboration
  9. MCP Integration
  10. Advanced Patterns
  11. Prompt-Based Hooks
  12. Community Examples
  13. Component-Scoped Hooks

Auto-Formatting

TypeScript with Biome

Auto-format TypeScript files after writing or editing.

Configuration (.claude/settings.json):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts|*.tsx)",
        "hooks": [
          {
            "type": "command",
            "command": "biome check --write \"$file\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Result: Every TypeScript file is automatically formatted with Biome after Claude writes or edits it.

Python with Black

Auto-format Python files with Black.

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.py)",
        "hooks": [
          {
            "type": "command",
            "command": "black \"$file\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Advanced version with multiple formatters:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.py)",
        "hooks": [
          {
            "type": "command",
            "command": "black \"$file\"",
            "timeout": 10
          },
          {
            "type": "command",
            "command": "isort \"$file\"",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Rust with rustfmt

Auto-format Rust code.

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.rs)",
        "hooks": [
          {
            "type": "command",
            "command": "rustfmt \"$file\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Multi-Language Formatter

Format multiple languages with appropriate tools.

Script (.claude/hooks/format-code.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE_PATH" ]]; then
  exit 0
fi

# Determine formatter based on extension
case "$FILE_PATH" in
  *.ts|*.tsx|*.js|*.jsx)
    if command -v biome &>/dev/null; then
      biome check --write "$FILE_PATH" 2>&1 || true
    fi
    ;;
  *.py)
    if command -v black &>/dev/null; then
      black "$FILE_PATH" 2>&1 || true
      isort "$FILE_PATH" 2>&1 || true
    fi
    ;;
  *.rs)
    if command -v rustfmt &>/dev/null; then
      rustfmt "$FILE_PATH" 2>&1 || true
    fi
    ;;
  *.go)
    if command -v gofmt &>/dev/null; then
      gofmt -w "$FILE_PATH" 2>&1 || true
    fi
    ;;
  *.md)
    if command -v prettier &>/dev/null; then
      prettier --write "$FILE_PATH" 2>&1 || true
    fi
    ;;
esac

exit 0

Configuration:

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

Validation Hooks

Bash Command Safety

Validate bash commands before execution.

Script (.claude/hooks/validate-bash.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Only validate Bash tool
if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
  exit 0
fi

# Dangerous patterns to block
DANGEROUS_PATTERNS=(
  '\brm\s+-rf\s+/'
  '\bmkfs\b'
  '\bdd\s+if='
  '\bformat\s+[cC]:'
  '>\s*/dev/sd[a-z]'
  '\b:()\{\s*:\|\:&\s*\};:'  # Fork bomb
  '\bchmod\s+777\s+/'
  '\bchown\s+.*\s+/'
)

# Check for dangerous patterns
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qE "$pattern"; then
    cat << EOF >&2
❌ Dangerous command blocked

Command: $COMMAND
Pattern: $pattern

This command could cause system damage and has been blocked.
EOF
    exit 2
  fi
done

# Deprecated command warnings
if echo "$COMMAND" | grep -qE '\bgrep\b'; then
  echo "⚠ Consider using 'rg' (ripgrep) instead of 'grep'" >&2
fi

if echo "$COMMAND" | grep -qE '\bfind\s+\S+\s+-name'; then
  echo "⚠ Consider using 'rg --files' or 'fd' instead of 'find'" >&2
fi

# Force push warning
if echo "$COMMAND" | grep -qE 'git\s+push\s+(--force|-f)'; then
  echo "⚠ Warning: Force push detected. Verify this is intentional." >&2
fi

echo "✓ Command validation passed"
exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

File Path Security

Prevent path traversal and sensitive file access.

Script (.claude/hooks/validate-paths.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE_PATH" ]]; then
  exit 0
fi

# Check for path traversal
if echo "$FILE_PATH" | grep -qE '\.\./'; then
  cat << EOF >&2
❌ Path traversal detected

File: $FILE_PATH

Paths containing '..' are not allowed for security reasons.
EOF
  exit 2
fi

# Check for sensitive system paths
SENSITIVE_PATTERNS=(
  '^/etc/'
  '^/root/'
  '^/home/[^/]+/\.ssh/'
  '^/var/log/'
  '^/sys/'
  '^/proc/'
)

for pattern in "${SENSITIVE_PATTERNS[@]}"; do
  if echo "$FILE_PATH" | grep -qE "$pattern"; then
    cat << EOF >&2
❌ Access to sensitive system path blocked

File: $FILE_PATH

This path is restricted for security reasons.
EOF
    exit 2
  fi
done

# Check for sensitive files
SENSITIVE_FILES=(
  '\.env$'
  '\.env\.'
  'id_rsa'
  'id_ed25519'
  '\.pem$'
  'credentials'
  'password'
  '\.key$'
  'secret'
)

for pattern in "${SENSITIVE_FILES[@]}"; do
  if echo "$FILE_PATH" | grep -qiE "$pattern"; then
    cat << EOF >&2
⚠ Warning: Accessing sensitive file

File: $FILE_PATH

This file may contain sensitive information. Ensure this is intentional.
EOF
    # Don't block, just warn
  fi
done

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|Read",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-paths.sh",
            "timeout": 3
          }
        ]
      }
    ]
  }
}

JSON Schema Validation

Validate JSON files against schemas.

Script (.claude/hooks/validate-json.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')

# Only validate JSON files
if [[ ! "$FILE_PATH" =~ \.json$ ]] || [[ -z "$CONTENT" ]]; then
  exit 0
fi

# Validate JSON syntax
if ! echo "$CONTENT" | jq empty 2>/dev/null; then
  echo "❌ Invalid JSON syntax" >&2
  exit 2
fi

# Validate specific files against schemas
case "$FILE_PATH" in
  *package.json)
    # Validate package.json has required fields
    if ! echo "$CONTENT" | jq -e '.name and .version' >/dev/null 2>&1; then
      echo "⚠ package.json missing required fields (name, version)" >&2
    fi
    ;;
  *tsconfig.json)
    # Validate tsconfig has compilerOptions
    if ! echo "$CONTENT" | jq -e '.compilerOptions' >/dev/null 2>&1; then
      echo "⚠ tsconfig.json missing compilerOptions" >&2
    fi
    ;;
  *.claude/settings.json)
    # Validate hooks structure if present
    if echo "$CONTENT" | jq -e '.hooks' >/dev/null 2>&1; then
      # Check each hook has required fields
      if ! echo "$CONTENT" | jq -e '.hooks | to_entries[] | .value[] | .matcher and .hooks' >/dev/null 2>&1; then
        echo "❌ Invalid hooks configuration" >&2
        exit 2
      fi
    fi
    ;;
esac

echo "✓ JSON validation passed"
exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write(*.json)",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-json.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

CI/CD Integration

Trigger Build on File Change

Trigger build when specific files are modified.

Script (.claude/hooks/trigger-build.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only trigger for source files
if [[ ! "$FILE_PATH" =~ (src/|lib/|pages/) ]]; then
  exit 0
fi

# Check if CI/CD is configured
if [[ ! -f ".github/workflows/build.yml" ]] && [[ ! -f ".gitlab-ci.yml" ]]; then
  exit 0
fi

# Create marker file for build trigger
MARKER_FILE=".build-needed"
echo "Build triggered by: $FILE_PATH" >> "$MARKER_FILE"

echo "✓ Build marker created: $MARKER_FILE"
echo "Run 'git add $MARKER_FILE && git commit' to trigger CI/CD build"

exit 0

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/trigger-build.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Run Tests After Code Changes

Automatically run tests when code changes.

Script (.claude/hooks/run-tests.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

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

# Skip test files themselves
if [[ "$FILE_PATH" =~ \.(test|spec)\. ]]; then
  exit 0
fi

echo "Running tests..."

# Detect test runner and run tests
if [[ -f "package.json" ]] && grep -q '"test"' package.json; then
  # Node.js project
  if command -v bun &>/dev/null; then
    bun test 2>&1 || {
      echo "⚠ Tests failed. Review failures before committing." >&2
      exit 0  # Don't block, just warn
    }
  elif command -v npm &>/dev/null; then
    npm test 2>&1 || {
      echo "⚠ Tests failed. Review failures before committing." >&2
      exit 0
    }
  fi
elif [[ -f "Cargo.toml" ]]; then
  # Rust project
  cargo test 2>&1 || {
    echo "⚠ Tests failed. Review failures before committing." >&2
    exit 0
  }
elif [[ -f "pytest.ini" ]] || [[ -f "pyproject.toml" ]]; then
  # Python project
  pytest 2>&1 || {
    echo "⚠ Tests failed. Review failures before committing." >&2
    exit 0
  }
fi

echo "✓ Tests passed"
exit 0

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts|*.tsx|*.py|*.rs)",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Update Documentation

Auto-update docs when code changes.

Script (.claude/hooks/update-docs.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only for public API files
if [[ ! "$FILE_PATH" =~ src/(index|api|public)\. ]]; then
  exit 0
fi

# Generate TypeScript docs
if [[ -f "tsconfig.json" ]] && command -v typedoc &>/dev/null; then
  echo "Generating TypeScript documentation..."
  typedoc --out docs/api src/index.ts 2>&1 || true
fi

# Generate Python docs
if [[ "$FILE_PATH" =~ \.py$ ]] && command -v pdoc &>/dev/null; then
  echo "Generating Python documentation..."
  pdoc --html --force --output-dir docs/api . 2>&1 || true
fi

# Generate Rust docs
if [[ "$FILE_PATH" =~ \.rs$ ]] && [[ -f "Cargo.toml" ]]; then
  echo "Generating Rust documentation..."
  cargo doc --no-deps 2>&1 || true
fi

exit 0

Configuration:

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

Notification Systems

Slack Integration

Send notifications to Slack.

Script (.claude/hooks/notify-slack.sh):

#!/usr/bin/env bash
set -euo pipefail

# Load webhook URL from .env if present (safe single-variable extraction)
if [[ -f ".env" ]]; then
  SLACK_WEBHOOK_URL=$(grep -E '^SLACK_WEBHOOK_URL=' .env | cut -d'=' -f2- | tr -d '"' | tr -d "'")
fi

WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"

if [[ -z "$WEBHOOK_URL" ]]; then
  exit 0
fi

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only notify for important operations
case "$TOOL_NAME" in
  Write|Edit)
    # Only notify for specific directories
    if [[ "$FILE_PATH" =~ (src/core/|src/api/|migrations/) ]]; then
      MESSAGE="🤖 Claude modified: \`$(basename "$FILE_PATH")\` in \`$(dirname "$FILE_PATH")\`"

      curl -X POST "$WEBHOOK_URL" \
        -H 'Content-Type: application/json' \
        -d "{\"text\":\"$MESSAGE\"}" \
        --silent --show-error 2>&1 || true
    fi
    ;;
esac

exit 0

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify-slack.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Email Notifications

Send email for important events.

Note: Configure a valid email address before use. The mail command will succeed even if the email is undeliverable, so verify your mail server setup.

Script (.claude/hooks/send-email.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')

# Only email for session events
if [[ "$HOOK_EVENT" != "SessionEnd" ]]; then
  exit 0
fi

REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')

# Check if email is configured
if ! command -v mail &>/dev/null; then
  exit 0
fi

# Send email
mail -s "Claude Code Session Ended: $REASON" user@example.com << EOF
Session ID: $SESSION_ID
Reason: $REASON
Timestamp: $(date -Iseconds)

This is an automated notification from Claude Code.
EOF

exit 0

Configuration:

{
  "hooks": {
    "SessionEnd": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/send-email.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Logging System

Comprehensive logging of all operations.

Script (.claude/hooks/log-operations.sh):

#!/usr/bin/env bash
set -euo pipefail

LOG_DIR="$CLAUDE_PROJECT_DIR/.claude/logs"
mkdir -p "$LOG_DIR"

INPUT=$(cat)
TIMESTAMP=$(date -Iseconds)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')

# Log to daily file
LOG_FILE="$LOG_DIR/$(date +%Y-%m-%d).log"

# Create log entry
LOG_ENTRY=$(jq -n \
  --arg ts "$TIMESTAMP" \
  --arg event "$HOOK_EVENT" \
  --arg tool "$TOOL_NAME" \
  --arg session "$SESSION_ID" \
  '{timestamp: $ts, event: $event, tool: $tool, session: $session}')

echo "$LOG_ENTRY" >> "$LOG_FILE"

# Rotate logs older than 30 days
find "$LOG_DIR" -name "*.log" -mtime +30 -delete 2>/dev/null || true

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-operations.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Context Injection

Add Timestamp

Add current timestamp to every prompt.

Script (.claude/hooks/add-timestamp.sh):

#!/usr/bin/env bash
set -euo pipefail

# Output current time context
cat << EOF
Current timestamp: $(date -Iseconds)
Current time: $(date '+%Y-%m-%d %H:%M:%S %Z')
Day of week: $(date '+%A')
EOF

exit 0

Configuration:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/add-timestamp.sh",
            "timeout": 1
          }
        ]
      }
    ]
  }
}

Add Git Context

Inject git status into prompt context.

Script (.claude/hooks/git-context.sh):

#!/usr/bin/env bash
set -euo pipefail

# Check if git repo
if ! git rev-parse --git-dir &>/dev/null; then
  exit 0
fi

cat << EOF
## Git Context

Branch: $(git branch --show-current 2>/dev/null || echo "detached")
Status: $(git status --short 2>/dev/null | wc -l | xargs) files modified
Last commit: $(git log -1 --oneline 2>/dev/null || echo "none")
Remote: $(git remote -v 2>/dev/null | head -1 | awk '{print $2}' || echo "none")
EOF

exit 0

Configuration:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/git-context.sh",
            "timeout": 3
          }
        ]
      }
    ]
  }
}

Add Environment Info

Inject environment and system information.

Script (.claude/hooks/env-context.sh):

#!/usr/bin/env bash
set -euo pipefail

cat << EOF
## Environment Context

OS: $(uname -s)
Architecture: $(uname -m)
Node: $(node --version 2>/dev/null || echo "not installed")
Bun: $(bun --version 2>/dev/null || echo "not installed")
Python: $(python3 --version 2>/dev/null || echo "not installed")
Rust: $(rustc --version 2>/dev/null || echo "not installed")

Working directory: $PWD
User: $USER
Shell: $SHELL
EOF

exit 0

Configuration:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/env-context.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Security Enforcement

Block Sensitive File Operations

Prevent operations on sensitive files.

Script (.claude/hooks/block-sensitive.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE_PATH" ]]; then
  exit 0
fi

# Define sensitive patterns
BLOCKED_PATTERNS=(
  '\.env$'
  '\.env\.'
  'credentials'
  'secrets'
  'id_rsa'
  'id_ed25519'
  '\.pem$'
  '\.key$'
  '\.p12$'
  'password'
  'token'
  '\.git/config$'
)

# Check against patterns
for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if echo "$FILE_PATH" | grep -qiE "$pattern"; then
    cat << EOF >&2
❌ Access to sensitive file blocked

File: $FILE_PATH
Pattern: $pattern

This file may contain sensitive information and is protected.
If you need to modify this file, do it manually outside Claude Code.
EOF
    exit 2
  fi
done

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|Read",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-sensitive.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Enforce File Permissions

Ensure proper file permissions.

Script (.claude/hooks/enforce-permissions.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE_PATH" ]] || [[ ! -f "$FILE_PATH" ]]; then
  exit 0
fi

# Ensure no world-writable files
if [[ -w "$FILE_PATH" ]] && [[ $(stat -f %A "$FILE_PATH" 2>/dev/null || stat -c %a "$FILE_PATH" 2>/dev/null) =~ [0-9][0-9]2$ ]]; then
  chmod o-w "$FILE_PATH"
  echo "⚠ Removed world-write permission from $FILE_PATH" >&2
fi

# Ensure scripts are executable
if [[ "$FILE_PATH" =~ \.(sh|bash|zsh)$ ]]; then
  if [[ ! -x "$FILE_PATH" ]]; then
    chmod +x "$FILE_PATH"
    echo "✓ Made script executable: $FILE_PATH"
  fi
fi

exit 0

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-permissions.sh",
            "timeout": 3
          }
        ]
      }
    ]
  }
}

Audit Trail

Create audit trail of all operations.

Script (.claude/hooks/audit-trail.sh):

#!/usr/bin/env bash
set -euo pipefail

AUDIT_FILE="$CLAUDE_PROJECT_DIR/.claude/audit.log"

INPUT=$(cat)
TIMESTAMP=$(date -Iseconds)
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "N/A"')
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')

# Create audit entry
AUDIT_ENTRY=$(jq -n \
  --arg ts "$TIMESTAMP" \
  --arg event "$HOOK_EVENT" \
  --arg tool "$TOOL_NAME" \
  --arg file "$FILE_PATH" \
  --arg session "$SESSION_ID" \
  --arg user "$USER" \
  --arg host "$HOSTNAME" \
  '{
    timestamp: $ts,
    event: $event,
    tool: $tool,
    file: $file,
    session: $session,
    user: $user,
    host: $host
  }')

# Append to audit log
echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"

# Keep only last 10000 lines
tail -n 10000 "$AUDIT_FILE" > "$AUDIT_FILE.tmp" && mv "$AUDIT_FILE.tmp" "$AUDIT_FILE"

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/audit-trail.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Multi-Hook Workflows

Complete TypeScript Workflow

Format, type-check, lint, and test TypeScript files.

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts|*.tsx)",
        "hooks": [
          {
            "type": "command",
            "command": "biome check --write \"$file\"",
            "timeout": 10
          },
          {
            "type": "command",
            "command": "tsc --noEmit \"$file\"",
            "timeout": 15
          },
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Python Development Workflow

Format, type-check, lint, and test Python files.

Configuration:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.py)",
        "hooks": [
          {
            "type": "command",
            "command": "black \"$file\"",
            "timeout": 10
          },
          {
            "type": "command",
            "command": "isort \"$file\"",
            "timeout": 5
          },
          {
            "type": "command",
            "command": "mypy \"$file\"",
            "timeout": 10
          },
          {
            "type": "command",
            "command": "pylint \"$file\"",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

Pre-Commit Workflow

Validate before allowing write operations.

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-paths.sh",
            "timeout": 3
          },
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-sensitive.sh",
            "timeout": 2
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh",
            "timeout": 15
          },
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/audit-trail.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Team Collaboration

Shared Team Hooks

Team-wide formatting and validation.

Project (.claude/settings.json):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts|*.tsx)",
        "hooks": [
          {
            "type": "command",
            "command": "biome check --write \"$file\"",
            "timeout": 10
          }
        ]
      },
      {
        "matcher": "Write|Edit(*.py)",
        "hooks": [
          {
            "type": "command",
            "command": "black \"$file\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Personal Overrides

Personal preferences that extend team hooks.

Personal (~/.claude/settings.json):

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "echo '👋 Welcome back!' && git status",
            "timeout": 3
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "echo '✅ Task completed at $(date +%H:%M)'",
            "timeout": 1
          }
        ]
      }
    ]
  }
}

MCP Integration

Log Memory Operations

Track MCP memory tool usage.

Script (.claude/hooks/log-memory.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

# Only for memory MCP tools
if [[ ! "$TOOL_NAME" =~ ^mcp__memory__ ]]; then
  exit 0
fi

OPERATION=$(echo "$TOOL_NAME" | sed 's/mcp__memory__//')
TIMESTAMP=$(date -Iseconds)

# Log the operation
echo "[$TIMESTAMP] Memory operation: $OPERATION" >> "$CLAUDE_PROJECT_DIR/.claude/memory-ops.log"

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/log-memory.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Validate GitHub Operations

Validate GitHub MCP operations.

Script (.claude/hooks/validate-github.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

# Only for GitHub MCP tools
if [[ ! "$TOOL_NAME" =~ ^mcp__github__ ]]; then
  exit 0
fi

# Warn about destructive operations
if [[ "$TOOL_NAME" =~ (delete|close|merge) ]]; then
  echo "⚠ Warning: Destructive GitHub operation: $TOOL_NAME" >&2
fi

exit 0

Configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__github__.*",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-github.sh",
            "timeout": 2
          }
        ]
      }
    ]
  }
}

Advanced Patterns

Conditional Hook Execution

Execute hooks only under certain conditions.

Script (.claude/hooks/conditional-format.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Only format during work hours (9 AM - 5 PM)
HOUR=$(date +%H)
if [[ $HOUR -lt 9 || $HOUR -gt 17 ]]; then
  echo "Skipping format outside work hours"
  exit 0
fi

# Only format files in src/ directory
if [[ ! "$FILE_PATH" =~ ^.*/src/ ]]; then
  exit 0
fi

# Run formatter
if [[ "$FILE_PATH" =~ \.ts$ ]]; then
  biome check --write "$FILE_PATH"
fi

exit 0

State Management

Track state across hook invocations.

Script (.claude/hooks/track-changes.sh):

#!/usr/bin/env bash
set -euo pipefail

STATE_FILE="$CLAUDE_PROJECT_DIR/.claude/hook-state.json"

# Initialize state if needed
if [[ ! -f "$STATE_FILE" ]]; then
  echo '{"files_modified": [], "total_operations": 0}' > "$STATE_FILE"
fi

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -n "$FILE_PATH" ]]; then
  # Update state
  STATE=$(cat "$STATE_FILE")
  STATE=$(echo "$STATE" | jq \
    --arg file "$FILE_PATH" \
    '.files_modified += [$file] | .files_modified |= unique | .total_operations += 1')
  echo "$STATE" > "$STATE_FILE"

  # Report
  TOTAL=$(echo "$STATE" | jq '.total_operations')
  echo "Total operations this session: $TOTAL"
fi

exit 0

Async Background Operations

Run expensive operations asynchronously.

Script (.claude/hooks/async-index.sh):

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Start background indexing
(
  sleep 1
  # Rebuild search index
  if command -v rg &>/dev/null; then
    rg --files > "$CLAUDE_PROJECT_DIR/.claude/file-index.txt" 2>/dev/null
  fi
  echo "Index updated: $(date -Iseconds)" >> "$CLAUDE_PROJECT_DIR/.claude/index.log"
) &

echo "✓ Background indexing started"
exit 0

Performance Monitoring

Track hook performance.

Script (.claude/hooks/perf-monitor.sh):

#!/usr/bin/env bash
set -euo pipefail

START_NS=$(date +%s%N)

# Original hook logic
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

# ... process ...

# Calculate duration
END_NS=$(date +%s%N)
DURATION_MS=$(( (END_NS - START_NS) / 1000000 ))

# Log performance
PERF_LOG="$CLAUDE_PROJECT_DIR/.claude/perf.log"
echo "$(date -Iseconds) | $TOOL_NAME | ${DURATION_MS}ms" >> "$PERF_LOG"

# Warn if slow
if [[ $DURATION_MS -gt 1000 ]]; then
  echo "⚠ Hook took ${DURATION_MS}ms (>1s)" >&2
fi

exit 0

Error Recovery

Robust error handling with recovery.

Script (.claude/hooks/robust-format.sh):

#!/usr/bin/env bash
set -euo pipefail

# Trap errors
trap 'echo "Error on line $LINENO" >&2; exit 1' ERR

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ -z "$FILE_PATH" ]]; then
  exit 0
fi

# Create backup before formatting
BACKUP="${FILE_PATH}.bak"
cp "$FILE_PATH" "$BACKUP"

# Try to format
if ! biome check --write "$FILE_PATH" 2>/dev/null; then
  # Restore backup on failure
  mv "$BACKUP" "$FILE_PATH"
  echo "⚠ Format failed, restored original file" >&2
  exit 1
fi

# Remove backup on success
rm -f "$BACKUP"
echo "✓ Formatted successfully"
exit 0

Prompt-Based Hooks

Prompt-based hooks use LLM reasoning for context-aware validation. Recommended for complex decisions.

Smart Security Validation

Use LLM to analyze file operations:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "prompt",
          "prompt": "Analyze this file operation for security issues:\n\n$TOOL_INPUT\n\nCheck for:\n1. Sensitive paths (/etc, ~/.ssh, .env files)\n2. Credentials or API keys in content\n3. Path traversal attempts (..)\n4. Executable file creation\n\nRespond with JSON: {\"decision\": \"allow|deny\", \"reason\": \"brief explanation\"}",
          "timeout": 30
        }]
      }
    ]
  }
}

Context-Aware Bash Validation

Evaluate command safety with reasoning:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "prompt",
          "prompt": "Evaluate if this bash command is safe to execute:\n\n$TOOL_INPUT\n\nConsider:\n1. Could it delete important files?\n2. Could it expose secrets?\n3. Could it modify system configuration?\n4. Is it appropriate for a development environment?\n\nRespond: {\"decision\": \"allow|deny\", \"reason\": \"...\"}",
          "timeout": 30
        }]
      }
    ]
  }
}

Task Completion Verification

Verify work quality before stopping:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [{
          "type": "prompt",
          "prompt": "Review the completed task. Consider:\n1. Were all requirements addressed?\n2. Were tests added or updated?\n3. Is there any unfinished work?\n4. Should the user be informed of anything?\n\nProvide a brief summary if there are concerns.",
          "timeout": 30
        }]
      }
    ]
  }
}

Community Examples

Real-world examples from the Claude Code community.

disler/claude-code-hooks-mastery

Comprehensive hook examples using Python with UV for dependency management.

Directory structure:

.claude/
├── hooks/
│   ├── user_prompt_submit.py   # Prompt validation and logging
│   ├── pre_tool_use.py         # Command blocking
│   ├── post_tool_use.py        # Tool completion logging
│   ├── notification.py         # TTS notifications
│   ├── stop.py                 # AI completion messages
│   ├── subagent_stop.py        # Subagent tracking
│   ├── pre_compact.py          # Transcript backup
│   └── session_start.py        # Context loading
└── settings.json

Configuration pattern:

{
  "UserPromptSubmit": [{
    "hooks": [{
      "type": "command",
      "command": "uv run .claude/hooks/user_prompt_submit.py --log-only"
    }]
  }],
  "PreToolUse": [{
    "matcher": "Bash",
    "hooks": [{
      "type": "command",
      "command": "uv run .claude/hooks/pre_tool_use.py"
    }]
  }]
}

Source: https://github.com/disler/claude-code-hooks-mastery

ChrisWiles/claude-code-showcase

Complete Claude Code configuration with hooks, skills, agents, and GitHub Actions.

Features:

  • Auto-format code on file changes
  • Run tests when test files change
  • Type-check TypeScript
  • Block edits on main branch
  • Skill matching for prompts

Branch protection hook:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "[ \"$(git branch --show-current)\" != \"main\" ] || exit 2",
        "timeout": 5
      }]
    }]
  }
}

Source: https://github.com/ChrisWiles/claude-code-showcase

GitButler Integration

GitButler provides hooks for automatic branch and commit management.

Configuration:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "but claude pre-tool",
        "timeout": 5
      }]
    }],
    "PostToolUse": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "but claude post-tool",
        "timeout": 5
      }]
    }],
    "Stop": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "but claude stop",
        "timeout": 10
      }]
    }]
  }
}

Source: https://docs.gitbutler.com/features/ai-integration/claude-code-hooks

Component-Scoped Hooks

Hooks defined in skills, agents, and commands frontmatter. Only active when the component is loaded.

Skill with Validation Hook

---
name: secure-coding
description: Security-focused coding skill
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: prompt
          prompt: "Validate this code change for security best practices..."
  PostToolUse:
    - matcher: "Write|Edit(*.ts)"
      hooks:
        - type: command
          command: "eslint --fix \"$file\""
---

# Secure Coding Skill

When active, this skill validates all code changes for security issues.

Agent with Completion Hook

---
name: code-reviewer
description: Reviews code for quality issues
model: sonnet
hooks:
  Stop:
    - matcher: "*"
      hooks:
        - type: prompt
          prompt: "Summarize the code review findings and severity levels."
---

# Code Reviewer Agent

Performs thorough code review with summarized findings.

Command with Context Hook

---
description: Deploy to staging environment
argument-hint: [component to deploy]
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./.claude/hooks/validate-deploy.sh"
---

# Deploy Command

Deploys the specified component to staging with pre-flight checks.

External Resources