Files
gh-alexanderop-claude-code-…/commands/create-hook.md
2025-11-29 17:52:01 +08:00

18 KiB

description: Create and configure Claude Code hooks with reference documentation argument-hint: [hook-type] [matcher] [command]

/create-hook

Purpose

Create and configure Claude Code hooks with reference documentation and interactive guidance.

Contract

Inputs:

  • $1 — HOOK_TYPE (optional: PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd)
  • $2 — MATCHER (optional: tool name pattern, e.g., "Bash", "Edit|Write", "*")
  • $3 — COMMAND (optional: shell command to execute)

Outputs: STATUS=<OK|FAIL> HOOK_FILE=<path>

Instructions

  1. Detect common use cases (hybrid approach):

    • Scan user's request for keywords: "eslint", "prettier", "format", "lint", "typescript", "test", "commit"
    • If detected, ask: "I can set up a production-ready [TOOL] hook. Would you like to use the template or create a custom hook?"
    • If template chosen: Generate external script file in .claude/hooks/ + settings.json entry
    • If custom or no match: Fall back to current workflow (steps 2-3)
  2. Determine hook configuration mode:

    • If no arguments provided: Show interactive menu of hook types with examples
    • If HOOK_TYPE provided: Guide user through creating that specific hook
    • If all arguments provided: Create hook directly
  3. Validate inputs:

    • HOOK_TYPE must be one of the valid hook events
    • MATCHER should be a valid tool name or pattern
    • COMMAND should be a valid shell command or path to external script
    • For external scripts: Ensure .claude/hooks/ directory exists
  4. Reference documentation: Review the Claude Code hooks documentation for best practices and examples:

    Hook Events:

    • PreToolUse: Runs before tool calls (can block them)
    • PostToolUse: Runs after tool calls complete
    • UserPromptSubmit: Runs when user submits a prompt
    • Notification: Runs when Claude Code sends notifications
    • Stop: Runs when Claude Code finishes responding
    • SubagentStop: Runs when subagent tasks complete
    • PreCompact: Runs before compact operations
    • SessionStart: Runs when session starts/resumes
    • SessionEnd: Runs when session ends
  5. Production-Ready Script Templates:

    When user requests common integrations, generate these external scripts in .claude/hooks/:

    ESLint Auto-Fix (run-eslint.sh):

    #!/usr/bin/env bash
    # Auto-fix JavaScript/TypeScript files with ESLint after edits
    set -euo pipefail
    
    # Extract file path from Claude's JSON payload
    file_path="$(jq -r '.tool_input.file_path // ""')"
    
    # Only process JS/TS files
    [[ "$file_path" =~ \.(js|jsx|ts|tsx)$ ]] || exit 0
    
    # Auto-detect package manager (prefer project's lock file)
    if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
      PM="pnpm exec"
    elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
      PM="yarn"
    else
      PM="npx"
    fi
    
    # Run ESLint with auto-fix from project root
    cd "$CLAUDE_PROJECT_DIR" && $PM eslint --fix "$file_path"
    

    Prettier Format (run-prettier.sh):

    #!/usr/bin/env bash
    # Format files with Prettier after edits
    set -euo pipefail
    
    file_path="$(jq -r '.tool_input.file_path // ""')"
    
    # Skip non-formattable files
    [[ "$file_path" =~ \.(js|jsx|ts|tsx|json|css|scss|md|html|yml|yaml)$ ]] || exit 0
    
    # Auto-detect package manager
    if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
      PM="pnpm exec"
    elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
      PM="yarn"
    else
      PM="npx"
    fi
    
    cd "$CLAUDE_PROJECT_DIR" && $PM prettier --write "$file_path"
    

    TypeScript Type Check (run-typescript.sh):

    #!/usr/bin/env bash
    # Run TypeScript type checker on TS/TSX file edits
    set -euo pipefail
    
    file_path="$(jq -r '.tool_input.file_path // ""')"
    
    # Only process TypeScript files
    [[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
    
    # Auto-detect package manager
    if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
      PM="pnpm exec"
    elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
      PM="yarn"
    else
      PM="npx"
    fi
    
    # Run tsc --noEmit to check types without emitting files
    cd "$CLAUDE_PROJECT_DIR" && $PM tsc --noEmit --pretty
    

    Run Affected Tests (run-tests.sh):

    #!/usr/bin/env bash
    # Run tests for modified files
    set -euo pipefail
    
    file_path="$(jq -r '.tool_input.file_path // ""')"
    
    # Only run tests for source files (not test files themselves)
    [[ "$file_path" =~ \.(test|spec)\.(js|ts|jsx|tsx)$ ]] && exit 0
    [[ "$file_path" =~ \.(js|jsx|ts|tsx)$ ]] || exit 0
    
    # Auto-detect test runner and package manager
    if [ -f "vitest.config.ts" ] || [ -f "vitest.config.js" ]; then
      TEST_CMD="vitest related --run"
    elif [ -f "jest.config.js" ] || [ -f "jest.config.ts" ]; then
      TEST_CMD="jest --findRelatedTests"
    else
      exit 0  # No test runner configured
    fi
    
    if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
      PM="pnpm exec"
    elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
      PM="yarn"
    else
      PM="npx"
    fi
    
    cd "$CLAUDE_PROJECT_DIR" && $PM $TEST_CMD "$file_path"
    

    Commit Message Validation (validate-commit.sh):

    #!/usr/bin/env bash
    # Validate commit messages follow conventional commits format
    set -euo pipefail
    
    # Extract commit message from tool input
    commit_msg="$(jq -r '.tool_input // ""')"
    
    # Check for conventional commit format: type(scope): message
    if ! echo "$commit_msg" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?: .+'; then
      echo "ERROR: Commit message must follow conventional commits format"
      echo "Expected: type(scope): description"
      echo "Got: $commit_msg"
      exit 2  # Block the commit
    fi
    

    Corresponding settings.json entries:

    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "comment": "Auto-fix with ESLint",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-eslint.sh"
              }
            ]
          }
        ]
      }
    }
    
  6. Best Practices for Hook Development:

    Always use shell safety headers:

    #!/usr/bin/env bash
    set -euo pipefail  # Exit on error, undefined vars, pipe failures
    

    Extract data from Claude's JSON payload:

    # File path (Edit/Write tools)
    file_path="$(jq -r '.tool_input.file_path // ""')"
    
    # Command (Bash tool)
    command="$(jq -r '.tool_input.command // ""')"
    
    # Tool name
    tool_name="$(jq -r '.tool_name // ""')"
    
    # Full tool input
    tool_input="$(jq -r '.tool_input' | jq -c .)"
    

    Use environment variables provided by Claude:

    $CLAUDE_PROJECT_DIR   # Project root directory
    $CLAUDE_USER_DIR      # User's ~/.claude directory
    $CLAUDE_SESSION_ID    # Current session identifier
    

    Efficient file extension filtering:

    # Good: Use bash regex matching
    [[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
    
    # Avoid: Spawning grep subprocess
    echo "$file_path" | grep -q '\.ts$' || exit 0
    

    Package manager auto-detection pattern:

    # Check lock files to match project's package manager
    if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
      PM="pnpm exec"
    elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
      PM="yarn"
    else
      PM="npx"  # Fallback to npx
    fi
    

    Exit codes matter:

    exit 0  # Success: Allow operation to continue
    exit 1  # Error: Log error but don't block
    exit 2  # Block: Prevent operation in PreToolUse hooks
    

    Performance considerations:

    • Avoid heavy operations in tight loops (e.g., don't run full test suite on every file edit)
    • Use file extension checks to skip irrelevant files early
    • Consider async/background execution for slow operations
    • Cache results when possible (e.g., dependency checks)

    When to use external scripts vs inline commands:

    • External scripts (.claude/hooks/*.sh): Complex logic, multiple steps, reusable patterns
    • Inline commands: Simple one-liners, quick jq filters, logging

    Example inline command:

    "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
    
  7. Common use cases and examples (updated with best practices):

    Logging Bash commands:

    {
      "hooks": {
        "PreToolUse": [{
          "matcher": "Bash",
          "hooks": [{
            "type": "command",
            "comment": "Log all Bash commands with descriptions",
            "command": "set -euo pipefail; cmd=$(jq -r '.tool_input.command // \"\"'); desc=$(jq -r '.tool_input.description // \"No description\"'); echo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $cmd - $desc\" >> \"$CLAUDE_USER_DIR/bash-command-log.txt\""
          }]
        }]
      }
    }
    

    Auto-format TypeScript files with Prettier:

    {
      "hooks": {
        "PostToolUse": [{
          "matcher": "Edit|Write",
          "hooks": [{
            "type": "command",
            "comment": "Auto-format TypeScript files after edits",
            "command": "set -euo pipefail; file_path=$(jq -r '.tool_input.file_path // \"\"'); [[ \"$file_path\" =~ \\.(ts|tsx)$ ]] || exit 0; cd \"$CLAUDE_PROJECT_DIR\" && npx prettier --write \"$file_path\""
          }]
        }]
      }
    }
    

    Block sensitive file edits:

    {
      "hooks": {
        "PreToolUse": [{
          "matcher": "Edit|Write",
          "hooks": [{
            "type": "command",
            "comment": "Prevent edits to sensitive files",
            "command": "set -euo pipefail; file_path=$(jq -r '.tool_input.file_path // \"\"'); if [[ \"$file_path\" =~ \\.(env|secrets|credentials) ]] || [[ \"$file_path\" == *\"package-lock.json\" ]] || [[ \"$file_path\" == *.git/* ]]; then echo \"ERROR: Cannot edit sensitive file: $file_path\" >&2; exit 2; fi"
          }]
        }]
      }
    }
    

    Desktop notifications:

    {
      "hooks": {
        "Notification": [{
          "matcher": "*",
          "hooks": [{
            "type": "command",
            "comment": "Send desktop notifications",
            "command": "if command -v notify-send >/dev/null 2>&1; then notify-send 'Claude Code' 'Awaiting your input'; elif command -v osascript >/dev/null 2>&1; then osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'; fi"
          }]
        }]
      }
    }
    
  8. Generic Patterns Reference:

    Pattern: Extract file path and check multiple extensions

    set -euo pipefail
    file_path=$(jq -r '.tool_input.file_path // ""')
    [[ "$file_path" =~ \.(js|ts|jsx|tsx|py|go|rs)$ ]] || exit 0
    # Your command here
    

    Pattern: Process multiple files from array

    set -euo pipefail
    jq -r '.tool_input.files[]?' | while IFS= read -r file; do
      [[ -f "$file" ]] && echo "Processing: $file"
      # Your processing logic here
    done
    

    Pattern: Conditional execution based on directory

    set -euo pipefail
    file_path=$(jq -r '.tool_input.file_path // ""')
    # Only process files in src/ directory
    [[ "$file_path" =~ ^src/ ]] || exit 0
    # Your command here
    

    Pattern: Extract and validate Bash command

    set -euo pipefail
    cmd=$(jq -r '.tool_input.command // ""')
    # Block dangerous commands
    if [[ "$cmd" =~ (rm -rf|mkfs|dd|:(){:|:&};:) ]]; then
      echo "ERROR: Dangerous command blocked" >&2
      exit 2
    fi
    

    Pattern: Background/async execution

    set -euo pipefail
    file_path=$(jq -r '.tool_input.file_path // ""')
    # Run slow operation in background, don't block Claude
    (cd "$CLAUDE_PROJECT_DIR" && npm run build "$file_path" &> /tmp/build.log) &
    

    Pattern: Conditional tool execution

    set -euo pipefail
    tool_name=$(jq -r '.tool_name // ""')
    case "$tool_name" in
      Edit|Write)
        file_path=$(jq -r '.tool_input.file_path // ""')
        echo "File modified: $file_path"
        ;;
      Bash)
        cmd=$(jq -r '.tool_input.command // ""')
        echo "Command executed: $cmd"
        ;;
    esac
    

    Pattern: Multi-tool chain with error handling

    set -euo pipefail
    file_path=$(jq -r '.tool_input.file_path // ""')
    [[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
    
    cd "$CLAUDE_PROJECT_DIR"
    
    # Run linter
    if ! npx eslint --fix "$file_path" 2>/dev/null; then
      echo "Warning: ESLint failed" >&2
    fi
    
    # Run formatter (always runs even if linter fails)
    npx prettier --write "$file_path" 2>/dev/null || true
    

    Pattern: Cache validation results

    set -euo pipefail
    file_path=$(jq -r '.tool_input.file_path // ""')
    cache_file="/tmp/claude-hook-cache-$(echo \"$file_path\" | md5sum | cut -d' ' -f1)"
    
    # Check cache freshness
    if [[ -f "$cache_file" ]] && [[ "$cache_file" -nt "$file_path" ]]; then
      cat "$cache_file"
      exit 0
    fi
    
    # Run expensive validation
    result=$(npx tsc --noEmit "$file_path" 2>&1)
    echo "$result" > "$cache_file"
    echo "$result"
    

    Pattern: Cross-platform compatibility

    set -euo pipefail
    
    # Detect OS and use appropriate commands
    case "$(uname -s)" in
      Darwin*)
        # macOS
        osascript -e 'display notification "Build complete"'
        ;;
      Linux*)
        # Linux
        notify-send "Build complete"
        ;;
      MINGW*|MSYS*|CYGWIN*)
        # Windows
        powershell -Command "New-BurntToastNotification -Text 'Build complete'"
        ;;
    esac
    
  9. Security considerations:

    • Hooks run automatically with your environment's credentials
    • Always review hook implementation before registering
    • Be cautious with hooks that execute external commands
    • Avoid hardcoding sensitive data in hook commands
    • Use exit code 2 to block operations in PreToolUse hooks
  10. Hook creation workflow:

For common tools (ESLint, Prettier, TypeScript, Tests):

  1. Detect tool name from user request keywords
  2. Ask user: "I can set up a production-ready [TOOL] hook. Use template or create custom?"
  3. If template:
    • Create .claude/hooks/ directory if needed
    • Generate appropriate script file (e.g., run-eslint.sh)
    • Make script executable (chmod +x)
    • Add settings.json entry with $CLAUDE_PROJECT_DIR reference
    • Verify package manager and dependencies exist
  4. Provide setup summary and next steps

For custom hooks:

  1. Determine appropriate hook event for the use case

  2. Define matcher pattern (specific tool name, regex like "Edit|Write", or "*" for all)

  3. Write shell command that processes JSON input via stdin

  4. Always start with set -euo pipefail for safety

  5. Use $CLAUDE_PROJECT_DIR and other environment variables

  6. Test hook command independently before registering

  7. Choose storage location:

    • .claude/settings.json - Project-specific, committed to git
    • .claude/settings.local.json - Project-specific, not committed
    • ~/.claude/settings.json - User-wide, all projects
  8. Register hook and reload with /hooks command

  9. Debugging hooks:

  • Run claude --debug to see hook execution logs
  • Use /hooks command to verify hook registration
  • Check hook configuration in settings files
  • Test hook command standalone with sample JSON:
    echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}}' | .claude/hooks/run-eslint.sh
    
  • Review Claude Code output for hook execution errors
  • Use jq to inspect JSON data passed to hooks
  • Check exit codes (0=success/continue, 1=error/log, 2=block operation)
  • Verify file permissions (chmod +x for external scripts)
  • Check that $CLAUDE_PROJECT_DIR resolves correctly
  1. Output:

Template mode:

  • Create external script file in .claude/hooks/[script-name].sh
  • Generate settings.json configuration snippet
  • Make script executable
  • Print: STATUS=OK HOOK_FILE=.claude/hooks/[script-name].sh SETTINGS=.claude/settings.json
  • Show: Next steps (reload hooks, test the hook)

Interactive/Direct mode:

  • Guide user through hook creation with prompts
  • Show the complete JSON configuration to add
  • Provide instructions for registering the hook
  • Print: STATUS=OK HOOK_FILE=~/.claude/settings.json or STATUS=OK HOOK_FILE=.claude/settings.local.json
  • Remind user to run /hooks to reload configuration

Constraints

  • External scripts must be executable (chmod +x)
  • Hooks must be valid shell commands or script paths
  • JSON structure must follow hooks schema
  • PreToolUse hooks can block operations with exit code 2
  • Hooks should be idempotent and handle errors gracefully
  • Consider performance impact of hooks in tight loops
  • Always use set -euo pipefail in bash scripts for safety
  • Use $CLAUDE_PROJECT_DIR for project-relative paths
  • Test hooks standalone before deploying

Examples

Template mode (recommended for common tools):

# User: "Set up ESLint to run on file edits"
/create-hook
# Detects "ESLint", offers template
# → Creates .claude/hooks/run-eslint.sh
# → Adds PostToolUse hook to settings.json
# → Makes script executable
# → STATUS=OK HOOK_FILE=.claude/hooks/run-eslint.sh

Interactive mode:

/create-hook
# Shows menu of:
#  1. Common tools (ESLint, Prettier, TypeScript, Tests)
#  2. Hook types (PreToolUse, PostToolUse, etc.)
#  3. Custom hook creation

Guided mode:

/create-hook PostToolUse
# Guides through creating a PostToolUse hook
# Asks for: matcher, command/script, storage location

Direct mode:

/create-hook PreToolUse "Bash" "set -euo pipefail; cmd=\$(jq -r '.tool_input.command'); echo \"[\$(date -Iseconds)] \$cmd\" >> \"\$CLAUDE_USER_DIR/bash.log\""
# Creates hook configuration directly with best practices

Reference

For complete documentation, see: https://docs.claude.com/en/hooks