Claude Cursor GitHub Copilot Skill

Hook Development

This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreT

LLM Mart · 0 points · 14 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tzachbon-smart-ralph-.agents_skills_Hook Development-4890dd3.zip · 21 KB
Part of tzachbon/smart-ralph — 44 skills

Install

skills CLI npx skills add https://github.com/tzachbon/smart-ralph/tree/main/.agents/skills/Hook Development
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tzachbon-smart-ralph@llmmart
Git git clone https://github.com/tzachbon/smart-ralph.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tzachbon/smart-ralph collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Hook Development for Claude Code Plugins

Overview

Hooks are event-driven automation scripts that execute in response to Claude Code events. Use hooks to validate operations, enforce policies, add context, and integrate external tools into workflows.

Key capabilities:

  • Validate tool calls before execution (PreToolUse)
  • React to tool results (PostToolUse)
  • Enforce completion standards (Stop, SubagentStop)
  • Load project context (SessionStart)
  • Automate workflows across the development lifecycle

Hook Types

Prompt-Based Hooks (Recommended)

Use LLM-driven decision making for context-aware validation:

{
  "type": "prompt",
  "prompt": "Evaluate if this tool use is appropriate: $TOOL_INPUT",
  "timeout": 30
}

Supported events: Stop, SubagentStop, UserPromptSubmit, PreToolUse

Benefits:

  • Context-aware decisions based on natural language reasoning
  • Flexible evaluation logic without bash scripting
  • Better edge case handling
  • Easier to maintain and extend

Command Hooks

Execute bash commands for deterministic checks:

{
  "type": "command",
  "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
  "timeout": 60
}

Use for:

  • Fast deterministic validations
  • File system operations
  • External tool integrations
  • Performance-critical checks

Hook Configuration Formats

Plugin hooks.json Format

For plugin hooks in hooks/hooks.json, use wrapper format:

{
  "description": "Brief explanation of hooks (optional)",
  "hooks": {
    "PreToolUse": [...],
    "Stop": [...],
    "SessionStart": [...]
  }
}

Key points:

  • description field is optional
  • hooks field is required wrapper containing actual hook events
  • This is the plugin-specific format

Example:

{
  "description": "Validation hooks for code quality",
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh"
          }
        ]
      }
    ]
  }
}

Settings Format (Direct)

For user settings in .claude/settings.json, use direct format:

{
  "PreToolUse": [...],
  "Stop": [...],
  "SessionStart": [...]
}

Key points:

  • No wrapper - events directly at top level
  • No description field
  • This is the settings format

Important: The examples below show the hook event structure that goes inside either format. For plugin hooks.json, wrap these in {"hooks": {...}}.

Hook Events

PreToolUse

Execute before any tool runs. Use to approve, deny, or modify tool calls.

Example (prompt-based):

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Validate file write safety. Check: system paths, credentials, path traversal, sensitive content. Return 'approve' or 'deny'."
        }
      ]
    }
  ]
}

Output for PreToolUse:

{
  "hookSpecificOutput": {
    "permissionDecision": "allow|deny|ask",
    "updatedInput": {"field": "modified_value"}
  },
  "systemMessage": "Explanation for Claude"
}

PostToolUse

Execute after tool completes. Use to react to results, provide feedback, or log.

Example:

{
  "PostToolUse": [
    {
      "matcher": "Edit",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Analyze edit result for potential issues: syntax errors, security vulnerabilities, breaking changes. Provide feedback."
        }
      ]
    }
  ]
}

Output behavior:

  • Exit 0: stdout shown in transcript
  • Exit 2: stderr fed back to Claude
  • systemMessage included in context

Stop

Execute when main agent considers stopping. Use to validate completeness.

Example:

{
  "Stop": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Verify task completion: tests run, build succeeded, questions answered. Return 'approve' to stop or 'block' with reason to continue."
        }
      ]
    }
  ]
}

Decision output:

{
  "decision": "approve|block",
  "reason": "Explanation",
  "systemMessage": "Additional context"
}

SubagentStop

Execute when subagent considers stopping. Use to ensure subagent completed its task.

Similar to Stop hook, but for subagents.

UserPromptSubmit

Execute when user submits a prompt. Use to add context, validate, or block prompts.

Example:

{
  "UserPromptSubmit": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Check if prompt requires security guidance. If discussing auth, permissions, or API security, return relevant warnings."
        }
      ]
    }
  ]
}

SessionStart

Execute when Claude Code session begins. Use to load context and set environment.

Example:

{
  "SessionStart": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
        }
      ]
    }
  ]
}

Special capability: Persist environment variables using $CLAUDE_ENV_FILE:

echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"

See examples/load-context.sh for complete example.

SessionEnd

Execute when session ends. Use for cleanup, logging, and state preservation.

PreCompact

Execute before context compaction. Use to add critical information to preserve.

Notification

Execute when Claude sends notifications. Use to react to user notifications.

Hook Output Format

Standard Output (All Hooks)

{
  "continue": true,
  "suppressOutput": false,
  "systemMessage": "Message for Claude"
}
  • continue: If false, halt processing (default true)
  • suppressOutput: Hide output from transcript (default false)
  • systemMessage: Message shown to Claude

Exit Codes

  • 0 - Success (stdout shown in transcript)
  • 2 - Blocking error (stderr fed back to Claude)
  • Other - Non-blocking error

Hook Input Format

All hooks receive JSON via stdin with common fields:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.txt",
  "cwd": "/current/working/dir",
  "permission_mode": "ask|allow",
  "hook_event_name": "PreToolUse"
}

Event-specific fields:

  • PreToolUse/PostToolUse: tool_name, tool_input, tool_result
  • UserPromptSubmit: user_prompt
  • Stop/SubagentStop: reason

Access fields in prompts using $TOOL_INPUT, $TOOL_RESULT, $USER_PROMPT, etc.

Environment Variables

Available in all command hooks:

  • $CLAUDE_PROJECT_DIR - Project root path
  • $CLAUDE_PLUGIN_ROOT - Plugin directory (use for portable paths)
  • $CLAUDE_ENV_FILE - SessionStart only: persist env vars here
  • $CLAUDE_CODE_REMOTE - Set if running in remote context

Always use $ in hook commands for portability:

{
  "type": "command",
  "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}

Plugin Hook Configuration

In plugins, define hooks in hooks/hooks.json:

{
  "PreToolUse": [
    {
      "matcher": "Write|Edit",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Validate file write safety"
        }
      ]
    }
  ],
  "Stop": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Verify task completion"
        }
      ]
    }
  ],
  "SessionStart": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh",
          "timeout": 10
        }
      ]
    }
  ]
}

Plugin hooks merge with user's hooks and run in parallel.

Matchers

Tool Name Matching

Exact match:

"matcher": "Write"

Multiple tools:

"matcher": "Read|Write|Edit"

Wildcard (all tools):

"matcher": "*"

Regex patterns:

"matcher": "mcp__.*__delete.*"  // All MCP delete tools

Note: Matchers are case-sensitive.

Common Patterns

// All MCP tools
"matcher": "mcp__.*"

// Specific plugin's MCP tools
"matcher": "mcp__plugin_asana_.*"

// All file operations
"matcher": "Read|Write|Edit"

// Bash commands only
"matcher": "Bash"

Security Best Practices

Input Validation

Always validate inputs in command hooks:

#!/bin/bash
set -euo pipefail

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

# Validate tool name format
if [[ ! "$tool_name" =~ ^[a-zA-Z0-9_]+$ ]]; then
  echo '{"decision": "deny", "reason": "Invalid tool name"}' >&2
  exit 2
fi

Path Safety

Check for path traversal and sensitive files:

file_path=$(echo "$input" | jq -r '.tool_input.file_path')

# Deny path traversal
if [[ "$file_path" == *".."* ]]; then
  echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2
  exit 2
fi

# Deny sensitive files
if [[ "$file_path" == *".env"* ]]; then
  echo '{"decision": "deny", "reason": "Sensitive file"}' >&2
  exit 2
fi

See examples/validate-write.sh and examples/validate-bash.sh for complete examples.

Quote All Variables

# GOOD: Quoted
echo "$file_path"
cd "$CLAUDE_PROJECT_DIR"

# BAD: Unquoted (injection risk)
echo $file_path
cd $CLAUDE_PROJECT_DIR

Set Appropriate Timeouts

{
  "type": "command",
  "command": "bash script.sh",
  "timeout": 10
}

Defaults: Command hooks (60s), Prompt hooks (30s)

Performance Considerations

Parallel Execution

All matching hooks run in parallel:

{
  "PreToolUse": [
    {
      "matcher": "Write",
      "hooks": [
        {"type": "command", "command": "check1.sh"},  // Parallel
        {"type": "command", "command": "check2.sh"},  // Parallel
        {"type": "prompt", "prompt": "Validate..."}   // Parallel
      ]
    }
  ]
}

Design implications:

  • Hooks don't see each other's output
  • Non-deterministic ordering
  • Design for independence

Optimization

  1. Use command hooks for quick deterministic checks
  2. Use prompt hooks for complex reasoning
  3. Cache validation results in temp files
  4. Minimize I/O in hot paths

Temporarily Active Hooks

Create hooks that activate conditionally by checking for a flag file or configuration:

Pattern: Flag file activation

#!/bin/bash
# Only active when flag file exists
FLAG_FILE="$CLAUDE_PROJECT_DIR/.enable-strict-validation"

if [ ! -f "$FLAG_FILE" ]; then
  # Flag not present, skip validation
  exit 0
fi

# Flag present, run validation
input=$(cat)
# ... validation logic ...

Pattern: Configuration-based activation

#!/bin/bash
# Check configuration for activation
CONFIG_FILE="$CLAUDE_PROJECT_DIR/.claude/plugin-config.json"

if [ -f "$CONFIG_FILE" ]; then
  enabled=$(jq -r '.strictMode // false' "$CONFIG_FILE")
  if [ "$enabled" != "true" ]; then
    exit 0  # Not enabled, skip
  fi
fi

# Enabled, run hook logic
input=$(cat)
# ... hook logic ...

Use cases:

  • Enable strict validation only when needed
  • Temporary debugging hooks
  • Project-specific hook behavior
  • Feature flags for hooks

Best practice: Document activation mechanism in plugin README so users know how to enable/disable temporary hooks.

Hook Lifecycle and Limitations

Hooks Load at Session Start

Important: Hooks are loaded when Claude Code session starts. Changes to hook configuration require restarting Claude Code.

Cannot hot-swap hooks:

  • Editing hooks/hooks.json won't affect current session
  • Adding new hook scripts won't be recognized
  • Changing hook commands/prompts won't update
  • Must restart Claude Code: exit and run claude again

To test hook changes:

  1. Edit hook configuration or scripts
  2. Exit Claude Code session
  3. Restart: claude or cc
  4. New hook configuration loads
  5. Test hooks with claude --debug

Hook Validation at Startup

Hooks are validated when Claude Code starts:

  • Invalid JSON in hooks.json causes loading failure
  • Missing scripts cause warnings
  • Syntax errors reported in debug mode

Use /hooks command to review loaded hooks in current session.

Debugging Hooks

Enable Debug Mode

claude --debug

Look for hook registration, execution logs, input/output JSON, and timing information.

Test Hook Scripts

Test command hooks directly:

echo '{"tool_name": "Write", "tool_input": {"file_path": "/test"}}' | \
  bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh

echo "Exit code: $?"

Validate JSON Output

Ensure hooks output valid JSON:

output=$(./your-hook.sh < test-input.json)
echo "$output" | jq .

Quick Reference

Hook Events Summary

Event When Use For
PreToolUse Before tool Validation, modification
PostToolUse After tool Feedback, logging
UserPromptSubmit User input Context, validation
Stop Agent stopping Completeness check
SubagentStop Subagent done Task validation
SessionStart Session begins Context loading
SessionEnd Session ends Cleanup, logging
PreCompact Before compact Preserve context
Notification User notified Logging, reactions

Best Practices

DO:

  • ✅ Use prompt-based hooks for complex logic
  • ✅ Use $ for portability
  • ✅ Validate all inputs in command hooks
  • ✅ Quote all bash variables
  • ✅ Set appropriate timeouts
  • ✅ Return structured JSON output
  • ✅ Test hooks thoroughly

DON'T:

  • ❌ Use hardcoded paths
  • ❌ Trust user input without validation
  • ❌ Create long-running hooks
  • ❌ Rely on hook execution order
  • ❌ Modify global state unpredictably
  • ❌ Log sensitive information

Additional Resources

Reference Files

For detailed patterns and advanced techniques, consult:

  • references/patterns.md - Common hook patterns (8+ proven patterns)
  • references/migration.md - Migrating from basic to advanced hooks
  • references/advanced.md - Advanced use cases and techniques

Example Hook Scripts

Working examples in examples/:

  • validate-write.sh - File write validation example
  • validate-bash.sh - Bash command validation example
  • load-context.sh - SessionStart context loading example

Utility Scripts

Development tools in scripts/:

  • validate-hook-schema.sh - Validate hooks.json structure and syntax
  • test-hook.sh - Test hooks with sample input before deployment
  • hook-linter.sh - Check hook scripts for common issues and best practices

External Resources

Implementation Workflow

To implement hooks in a plugin:

  1. Identify events to hook into (PreToolUse, Stop, SessionStart, etc.)
  2. Decide between prompt-based (flexible) or command (deterministic) hooks
  3. Write hook configuration in hooks/hooks.json
  4. For command hooks, create hook scripts
  5. Use $ for all file references
  6. Validate configuration with scripts/validate-hook-schema.sh hooks/hooks.json
  7. Test hooks with scripts/test-hook.sh before deployment
  8. Test in Claude Code with claude --debug
  9. Document hooks in plugin README

Focus on prompt-based hooks for most use cases. Reserve command hooks for performance-critical or deterministic checks.

Files (smart-ralph)
  • examples
    • load-context.sh 1.7 KB
      #!/bin/bash
      # Example SessionStart hook for loading project context
      # This script detects project type and sets environment variables
      
      set -euo pipefail
      
      # Navigate to project directory
      cd "$CLAUDE_PROJECT_DIR" || exit 1
      
      echo "Loading project context..."
      
      # Detect project type and set environment
      if [ -f "package.json" ]; then
        echo "📦 Node.js project detected"
        echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
      
        # Check if TypeScript
        if [ -f "tsconfig.json" ]; then
          echo "export USES_TYPESCRIPT=true" >> "$CLAUDE_ENV_FILE"
        fi
      
      elif [ -f "Cargo.toml" ]; then
        echo "🦀 Rust project detected"
        echo "export PROJECT_TYPE=rust" >> "$CLAUDE_ENV_FILE"
      
      elif [ -f "go.mod" ]; then
        echo "🐹 Go project detected"
        echo "export PROJECT_TYPE=go" >> "$CLAUDE_ENV_FILE"
      
      elif [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
        echo "🐍 Python project detected"
        echo "export PROJECT_TYPE=python" >> "$CLAUDE_ENV_FILE"
      
      elif [ -f "pom.xml" ]; then
        echo "☕ Java (Maven) project detected"
        echo "export PROJECT_TYPE=java" >> "$CLAUDE_ENV_FILE"
        echo "export BUILD_SYSTEM=maven" >> "$CLAUDE_ENV_FILE"
      
      elif [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
        echo "☕ Java/Kotlin (Gradle) project detected"
        echo "export PROJECT_TYPE=java" >> "$CLAUDE_ENV_FILE"
        echo "export BUILD_SYSTEM=gradle" >> "$CLAUDE_ENV_FILE"
      
      else
        echo "❓ Unknown project type"
        echo "export PROJECT_TYPE=unknown" >> "$CLAUDE_ENV_FILE"
      fi
      
      # Check for CI configuration
      if [ -f ".github/workflows" ] || [ -f ".gitlab-ci.yml" ] || [ -f ".circleci/config.yml" ]; then
        echo "export HAS_CI=true" >> "$CLAUDE_ENV_FILE"
      fi
      
      echo "Project context loaded successfully"
      exit 0
      
    • validate-bash.sh 1.3 KB
      #!/bin/bash
      # Example PreToolUse hook for validating Bash commands
      # This script demonstrates bash command validation patterns
      
      set -euo pipefail
      
      # Read input from stdin
      input=$(cat)
      
      # Extract command
      command=$(echo "$input" | jq -r '.tool_input.command // empty')
      
      # Validate command exists
      if [ -z "$command" ]; then
        echo '{"continue": true}' # No command to validate
        exit 0
      fi
      
      # Check for obviously safe commands (quick approval)
      if [[ "$command" =~ ^(ls|pwd|echo|date|whoami)(\s|$) ]]; then
        exit 0
      fi
      
      # Check for destructive operations
      if [[ "$command" == *"rm -rf"* ]] || [[ "$command" == *"rm -fr"* ]]; then
        echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Dangerous command detected: rm -rf"}' >&2
        exit 2
      fi
      
      # Check for other dangerous commands
      if [[ "$command" == *"dd if="* ]] || [[ "$command" == *"mkfs"* ]] || [[ "$command" == *"> /dev/"* ]]; then
        echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Dangerous system operation detected"}' >&2
        exit 2
      fi
      
      # Check for privilege escalation
      if [[ "$command" == sudo* ]] || [[ "$command" == su* ]]; then
        echo '{"hookSpecificOutput": {"permissionDecision": "ask"}, "systemMessage": "Command requires elevated privileges"}' >&2
        exit 2
      fi
      
      # Approve the operation
      exit 0
      
    • validate-write.sh 1.2 KB
      #!/bin/bash
      # Example PreToolUse hook for validating Write/Edit operations
      # This script demonstrates file write validation patterns
      
      set -euo pipefail
      
      # Read input from stdin
      input=$(cat)
      
      # Extract file path and content
      file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
      
      # Validate path exists
      if [ -z "$file_path" ]; then
        echo '{"continue": true}' # No path to validate
        exit 0
      fi
      
      # Check for path traversal
      if [[ "$file_path" == *".."* ]]; then
        echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Path traversal detected in: '"$file_path"'"}' >&2
        exit 2
      fi
      
      # Check for system directories
      if [[ "$file_path" == /etc/* ]] || [[ "$file_path" == /sys/* ]] || [[ "$file_path" == /usr/* ]]; then
        echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Cannot write to system directory: '"$file_path"'"}' >&2
        exit 2
      fi
      
      # Check for sensitive files
      if [[ "$file_path" == *.env ]] || [[ "$file_path" == *secret* ]] || [[ "$file_path" == *credentials* ]]; then
        echo '{"hookSpecificOutput": {"permissionDecision": "ask"}, "systemMessage": "Writing to potentially sensitive file: '"$file_path"'"}' >&2
        exit 2
      fi
      
      # Approve the operation
      exit 0
      
  • references
    • advanced.md 9.9 KB
      # Advanced Hook Use Cases
      
      This reference covers advanced hook patterns and techniques for sophisticated automation workflows.
      
      ## Multi-Stage Validation
      
      Combine command and prompt hooks for layered validation:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/quick-check.sh",
                "timeout": 5
              },
              {
                "type": "prompt",
                "prompt": "Deep analysis of bash command: $TOOL_INPUT",
                "timeout": 15
              }
            ]
          }
        ]
      }
      ```
      
      **Use case:** Fast deterministic checks followed by intelligent analysis
      
      **Example quick-check.sh:**
      ```bash
      #!/bin/bash
      input=$(cat)
      command=$(echo "$input" | jq -r '.tool_input.command')
      
      # Immediate approval for safe commands
      if [[ "$command" =~ ^(ls|pwd|echo|date|whoami)$ ]]; then
        exit 0
      fi
      
      # Let prompt hook handle complex cases
      exit 0
      ```
      
      The command hook quickly approves obviously safe commands, while the prompt hook analyzes everything else.
      
      ## Conditional Hook Execution
      
      Execute hooks based on environment or context:
      
      ```bash
      #!/bin/bash
      # Only run in CI environment
      if [ -z "$CI" ]; then
        echo '{"continue": true}' # Skip in non-CI
        exit 0
      fi
      
      # Run validation logic in CI
      input=$(cat)
      # ... validation code ...
      ```
      
      **Use cases:**
      - Different behavior in CI vs local development
      - Project-specific validation
      - User-specific rules
      
      **Example: Skip certain checks for trusted users:**
      ```bash
      #!/bin/bash
      # Skip detailed checks for admin users
      if [ "$USER" = "admin" ]; then
        exit 0
      fi
      
      # Full validation for other users
      input=$(cat)
      # ... validation code ...
      ```
      
      ## Hook Chaining via State
      
      Share state between hooks using temporary files:
      
      ```bash
      # Hook 1: Analyze and save state
      #!/bin/bash
      input=$(cat)
      command=$(echo "$input" | jq -r '.tool_input.command')
      
      # Analyze command
      risk_level=$(calculate_risk "$command")
      echo "$risk_level" > /tmp/hook-state-$$
      
      exit 0
      ```
      
      ```bash
      # Hook 2: Use saved state
      #!/bin/bash
      risk_level=$(cat /tmp/hook-state-$$ 2>/dev/null || echo "unknown")
      
      if [ "$risk_level" = "high" ]; then
        echo "High risk operation detected" >&2
        exit 2
      fi
      ```
      
      **Important:** This only works for sequential hook events (e.g., PreToolUse then PostToolUse), not parallel hooks.
      
      ## Dynamic Hook Configuration
      
      Modify hook behavior based on project configuration:
      
      ```bash
      #!/bin/bash
      cd "$CLAUDE_PROJECT_DIR" || exit 1
      
      # Read project-specific config
      if [ -f ".claude-hooks-config.json" ]; then
        strict_mode=$(jq -r '.strict_mode' .claude-hooks-config.json)
      
        if [ "$strict_mode" = "true" ]; then
          # Apply strict validation
          # ...
        else
          # Apply lenient validation
          # ...
        fi
      fi
      ```
      
      **Example .claude-hooks-config.json:**
      ```json
      {
        "strict_mode": true,
        "allowed_commands": ["ls", "pwd", "grep"],
        "forbidden_paths": ["/etc", "/sys"]
      }
      ```
      
      ## Context-Aware Prompt Hooks
      
      Use transcript and session context for intelligent decisions:
      
      ```json
      {
        "Stop": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Review the full transcript at $TRANSCRIPT_PATH. Check: 1) Were tests run after code changes? 2) Did the build succeed? 3) Were all user questions answered? 4) Is there any unfinished work? Return 'approve' only if everything is complete."
              }
            ]
          }
        ]
      }
      ```
      
      The LLM can read the transcript file and make context-aware decisions.
      
      ## Performance Optimization
      
      ### Caching Validation Results
      
      ```bash
      #!/bin/bash
      input=$(cat)
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      cache_key=$(echo -n "$file_path" | md5sum | cut -d' ' -f1)
      cache_file="/tmp/hook-cache-$cache_key"
      
      # Check cache
      if [ -f "$cache_file" ]; then
        cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || stat -c%Y "$cache_file")))
        if [ "$cache_age" -lt 300 ]; then  # 5 minute cache
          cat "$cache_file"
          exit 0
        fi
      fi
      
      # Perform validation
      result='{"decision": "approve"}'
      
      # Cache result
      echo "$result" > "$cache_file"
      echo "$result"
      ```
      
      ### Parallel Execution Optimization
      
      Since hooks run in parallel, design them to be independent:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Write",
            "hooks": [
              {
                "type": "command",
                "command": "bash check-size.sh",      // Independent
                "timeout": 2
              },
              {
                "type": "command",
                "command": "bash check-path.sh",      // Independent
                "timeout": 2
              },
              {
                "type": "prompt",
                "prompt": "Check content safety",     // Independent
                "timeout": 10
              }
            ]
          }
        ]
      }
      ```
      
      All three hooks run simultaneously, reducing total latency.
      
      ## Cross-Event Workflows
      
      Coordinate hooks across different events:
      
      **SessionStart - Set up tracking:**
      ```bash
      #!/bin/bash
      # Initialize session tracking
      echo "0" > /tmp/test-count-$$
      echo "0" > /tmp/build-count-$$
      ```
      
      **PostToolUse - Track events:**
      ```bash
      #!/bin/bash
      input=$(cat)
      tool_name=$(echo "$input" | jq -r '.tool_name')
      
      if [ "$tool_name" = "Bash" ]; then
        command=$(echo "$input" | jq -r '.tool_result')
        if [[ "$command" == *"test"* ]]; then
          count=$(cat /tmp/test-count-$$ 2>/dev/null || echo "0")
          echo $((count + 1)) > /tmp/test-count-$$
        fi
      fi
      ```
      
      **Stop - Verify based on tracking:**
      ```bash
      #!/bin/bash
      test_count=$(cat /tmp/test-count-$$ 2>/dev/null || echo "0")
      
      if [ "$test_count" -eq 0 ]; then
        echo '{"decision": "block", "reason": "No tests were run"}' >&2
        exit 2
      fi
      ```
      
      ## Integration with External Systems
      
      ### Slack Notifications
      
      ```bash
      #!/bin/bash
      input=$(cat)
      tool_name=$(echo "$input" | jq -r '.tool_name')
      decision="blocked"
      
      # Send notification to Slack
      curl -X POST "$SLACK_WEBHOOK" \
        -H 'Content-Type: application/json' \
        -d "{\"text\": \"Hook ${decision} ${tool_name} operation\"}" \
        2>/dev/null
      
      echo '{"decision": "deny"}' >&2
      exit 2
      ```
      
      ### Database Logging
      
      ```bash
      #!/bin/bash
      input=$(cat)
      
      # Log to database
      psql "$DATABASE_URL" -c "INSERT INTO hook_logs (event, data) VALUES ('PreToolUse', '$input')" \
        2>/dev/null
      
      exit 0
      ```
      
      ### Metrics Collection
      
      ```bash
      #!/bin/bash
      input=$(cat)
      tool_name=$(echo "$input" | jq -r '.tool_name')
      
      # Send metrics to monitoring system
      echo "hook.pretooluse.${tool_name}:1|c" | nc -u -w1 statsd.local 8125
      
      exit 0
      ```
      
      ## Security Patterns
      
      ### Rate Limiting
      
      ```bash
      #!/bin/bash
      input=$(cat)
      command=$(echo "$input" | jq -r '.tool_input.command')
      
      # Track command frequency
      rate_file="/tmp/hook-rate-$$"
      current_minute=$(date +%Y%m%d%H%M)
      
      if [ -f "$rate_file" ]; then
        last_minute=$(head -1 "$rate_file")
        count=$(tail -1 "$rate_file")
      
        if [ "$current_minute" = "$last_minute" ]; then
          if [ "$count" -gt 10 ]; then
            echo '{"decision": "deny", "reason": "Rate limit exceeded"}' >&2
            exit 2
          fi
          count=$((count + 1))
        else
          count=1
        fi
      else
        count=1
      fi
      
      echo "$current_minute" > "$rate_file"
      echo "$count" >> "$rate_file"
      
      exit 0
      ```
      
      ### Audit Logging
      
      ```bash
      #!/bin/bash
      input=$(cat)
      tool_name=$(echo "$input" | jq -r '.tool_name')
      timestamp=$(date -Iseconds)
      
      # Append to audit log
      echo "$timestamp | $USER | $tool_name | $input" >> ~/.claude/audit.log
      
      exit 0
      ```
      
      ### Secret Detection
      
      ```bash
      #!/bin/bash
      input=$(cat)
      content=$(echo "$input" | jq -r '.tool_input.content')
      
      # Check for common secret patterns
      if echo "$content" | grep -qE "(api[_-]?key|password|secret|token).{0,20}['\"]?[A-Za-z0-9]{20,}"; then
        echo '{"decision": "deny", "reason": "Potential secret detected in content"}' >&2
        exit 2
      fi
      
      exit 0
      ```
      
      ## Testing Advanced Hooks
      
      ### Unit Testing Hook Scripts
      
      ```bash
      # test-hook.sh
      #!/bin/bash
      
      # Test 1: Approve safe command
      result=$(echo '{"tool_input": {"command": "ls"}}' | bash validate-bash.sh)
      if [ $? -eq 0 ]; then
        echo "✓ Test 1 passed"
      else
        echo "✗ Test 1 failed"
      fi
      
      # Test 2: Block dangerous command
      result=$(echo '{"tool_input": {"command": "rm -rf /"}}' | bash validate-bash.sh)
      if [ $? -eq 2 ]; then
        echo "✓ Test 2 passed"
      else
        echo "✗ Test 2 failed"
      fi
      ```
      
      ### Integration Testing
      
      Create test scenarios that exercise the full hook workflow:
      
      ```bash
      # integration-test.sh
      #!/bin/bash
      
      # Set up test environment
      export CLAUDE_PROJECT_DIR="/tmp/test-project"
      export CLAUDE_PLUGIN_ROOT="$(pwd)"
      mkdir -p "$CLAUDE_PROJECT_DIR"
      
      # Test SessionStart hook
      echo '{}' | bash hooks/session-start.sh
      if [ -f "/tmp/session-initialized" ]; then
        echo "✓ SessionStart hook works"
      else
        echo "✗ SessionStart hook failed"
      fi
      
      # Clean up
      rm -rf "$CLAUDE_PROJECT_DIR"
      ```
      
      ## Best Practices for Advanced Hooks
      
      1. **Keep hooks independent**: Don't rely on execution order
      2. **Use timeouts**: Set appropriate limits for each hook type
      3. **Handle errors gracefully**: Provide clear error messages
      4. **Document complexity**: Explain advanced patterns in README
      5. **Test thoroughly**: Cover edge cases and failure modes
      6. **Monitor performance**: Track hook execution time
      7. **Version configuration**: Use version control for hook configs
      8. **Provide escape hatches**: Allow users to bypass hooks when needed
      
      ## Common Pitfalls
      
      ### ❌ Assuming Hook Order
      
      ```bash
      # BAD: Assumes hooks run in specific order
      # Hook 1 saves state, Hook 2 reads it
      # This can fail because hooks run in parallel!
      ```
      
      ### ❌ Long-Running Hooks
      
      ```bash
      # BAD: Hook takes 2 minutes to run
      sleep 120
      # This will timeout and block the workflow
      ```
      
      ### ❌ Uncaught Exceptions
      
      ```bash
      # BAD: Script crashes on unexpected input
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      cat "$file_path"  # Fails if file doesn't exist
      ```
      
      ### ✅ Proper Error Handling
      
      ```bash
      # GOOD: Handles errors gracefully
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      if [ ! -f "$file_path" ]; then
        echo '{"continue": true, "systemMessage": "File not found, skipping check"}' >&2
        exit 0
      fi
      ```
      
      ## Conclusion
      
      Advanced hook patterns enable sophisticated automation while maintaining reliability and performance. Use these techniques when basic hooks are insufficient, but always prioritize simplicity and maintainability.
      
    • migration.md 8.1 KB
      # Migrating from Basic to Advanced Hooks
      
      This guide shows how to migrate from basic command hooks to advanced prompt-based hooks for better maintainability and flexibility.
      
      ## Why Migrate?
      
      Prompt-based hooks offer several advantages:
      
      - **Natural language reasoning**: LLM understands context and intent
      - **Better edge case handling**: Adapts to unexpected scenarios
      - **No bash scripting required**: Simpler to write and maintain
      - **More flexible validation**: Can handle complex logic without coding
      
      ## Migration Example: Bash Command Validation
      
      ### Before (Basic Command Hook)
      
      **Configuration:**
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "bash validate-bash.sh"
              }
            ]
          }
        ]
      }
      ```
      
      **Script (validate-bash.sh):**
      ```bash
      #!/bin/bash
      input=$(cat)
      command=$(echo "$input" | jq -r '.tool_input.command')
      
      # Hard-coded validation logic
      if [[ "$command" == *"rm -rf"* ]]; then
        echo "Dangerous command detected" >&2
        exit 2
      fi
      ```
      
      **Problems:**
      - Only checks for exact "rm -rf" pattern
      - Doesn't catch variations like `rm -fr` or `rm -r -f`
      - Misses other dangerous commands (`dd`, `mkfs`, etc.)
      - No context awareness
      - Requires bash scripting knowledge
      
      ### After (Advanced Prompt Hook)
      
      **Configuration:**
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Command: $TOOL_INPUT.command. Analyze for: 1) Destructive operations (rm -rf, dd, mkfs, etc) 2) Privilege escalation (sudo) 3) Network operations without user consent. Return 'approve' or 'deny' with explanation.",
                "timeout": 15
              }
            ]
          }
        ]
      }
      ```
      
      **Benefits:**
      - Catches all variations and patterns
      - Understands intent, not just literal strings
      - No script file needed
      - Easy to extend with new criteria
      - Context-aware decisions
      - Natural language explanation in denial
      
      ## Migration Example: File Write Validation
      
      ### Before (Basic Command Hook)
      
      **Configuration:**
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Write",
            "hooks": [
              {
                "type": "command",
                "command": "bash validate-write.sh"
              }
            ]
          }
        ]
      }
      ```
      
      **Script (validate-write.sh):**
      ```bash
      #!/bin/bash
      input=$(cat)
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      
      # Check for path traversal
      if [[ "$file_path" == *".."* ]]; then
        echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2
        exit 2
      fi
      
      # Check for system paths
      if [[ "$file_path" == "/etc/"* ]] || [[ "$file_path" == "/sys/"* ]]; then
        echo '{"decision": "deny", "reason": "System file"}' >&2
        exit 2
      fi
      ```
      
      **Problems:**
      - Hard-coded path patterns
      - Doesn't understand symlinks
      - Missing edge cases (e.g., `/etc` vs `/etc/`)
      - No consideration of file content
      
      ### After (Advanced Prompt Hook)
      
      **Configuration:**
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Write|Edit",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "File path: $TOOL_INPUT.file_path. Content preview: $TOOL_INPUT.content (first 200 chars). Verify: 1) Not system directories (/etc, /sys, /usr) 2) Not credentials (.env, tokens, secrets) 3) No path traversal 4) Content doesn't expose secrets. Return 'approve' or 'deny'."
              }
            ]
          }
        ]
      }
      ```
      
      **Benefits:**
      - Context-aware (considers content too)
      - Handles symlinks and edge cases
      - Natural understanding of "system directories"
      - Can detect secrets in content
      - Easy to extend criteria
      
      ## When to Keep Command Hooks
      
      Command hooks still have their place:
      
      ### 1. Deterministic Performance Checks
      
      ```bash
      #!/bin/bash
      # Check file size quickly
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      size=$(stat -f%z "$file_path" 2>/dev/null || stat -c%s "$file_path" 2>/dev/null)
      
      if [ "$size" -gt 10000000 ]; then
        echo '{"decision": "deny", "reason": "File too large"}' >&2
        exit 2
      fi
      ```
      
      **Use command hooks when:** Validation is purely mathematical or deterministic.
      
      ### 2. External Tool Integration
      
      ```bash
      #!/bin/bash
      # Run security scanner
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      scan_result=$(security-scanner "$file_path")
      
      if [ "$?" -ne 0 ]; then
        echo "Security scan failed: $scan_result" >&2
        exit 2
      fi
      ```
      
      **Use command hooks when:** Integrating with external tools that provide yes/no answers.
      
      ### 3. Very Fast Checks (< 50ms)
      
      ```bash
      #!/bin/bash
      # Quick regex check
      command=$(echo "$input" | jq -r '.tool_input.command')
      
      if [[ "$command" =~ ^(ls|pwd|echo)$ ]]; then
        exit 0  # Safe commands
      fi
      ```
      
      **Use command hooks when:** Performance is critical and logic is simple.
      
      ## Hybrid Approach
      
      Combine both for multi-stage validation:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/quick-check.sh",
                "timeout": 5
              },
              {
                "type": "prompt",
                "prompt": "Deep analysis of bash command: $TOOL_INPUT",
                "timeout": 15
              }
            ]
          }
        ]
      }
      ```
      
      The command hook does fast deterministic checks, while the prompt hook handles complex reasoning.
      
      ## Migration Checklist
      
      When migrating hooks:
      
      - [ ] Identify the validation logic in the command hook
      - [ ] Convert hard-coded patterns to natural language criteria
      - [ ] Test with edge cases the old hook missed
      - [ ] Verify LLM understands the intent
      - [ ] Set appropriate timeout (usually 15-30s for prompt hooks)
      - [ ] Document the new hook in README
      - [ ] Remove or archive old script files
      
      ## Migration Tips
      
      1. **Start with one hook**: Don't migrate everything at once
      2. **Test thoroughly**: Verify prompt hook catches what command hook caught
      3. **Look for improvements**: Use migration as opportunity to enhance validation
      4. **Keep scripts for reference**: Archive old scripts in case you need to reference the logic
      5. **Document reasoning**: Explain why prompt hook is better in README
      
      ## Complete Migration Example
      
      ### Original Plugin Structure
      
      ```
      my-plugin/
      ├── .claude-plugin/plugin.json
      ├── hooks/hooks.json
      └── scripts/
          ├── validate-bash.sh
          ├── validate-write.sh
          └── check-tests.sh
      ```
      
      ### After Migration
      
      ```
      my-plugin/
      ├── .claude-plugin/plugin.json
      ├── hooks/hooks.json      # Now uses prompt hooks
      └── scripts/              # Archive or delete
          └── archive/
              ├── validate-bash.sh
              ├── validate-write.sh
              └── check-tests.sh
      ```
      
      ### Updated hooks.json
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Validate bash command safety: destructive ops, privilege escalation, network access"
              }
            ]
          },
          {
            "matcher": "Write|Edit",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Validate file write safety: system paths, credentials, path traversal, content secrets"
              }
            ]
          }
        ],
        "Stop": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Verify tests were run if code was modified"
              }
            ]
          }
        ]
      }
      ```
      
      **Result:** Simpler, more maintainable, more powerful.
      
      ## Common Migration Patterns
      
      ### Pattern: String Contains → Natural Language
      
      **Before:**
      ```bash
      if [[ "$command" == *"sudo"* ]]; then
        echo "Privilege escalation" >&2
        exit 2
      fi
      ```
      
      **After:**
      ```
      "Check for privilege escalation (sudo, su, etc)"
      ```
      
      ### Pattern: Regex → Intent
      
      **Before:**
      ```bash
      if [[ "$file" =~ \.(env|secret|key|token)$ ]]; then
        echo "Credential file" >&2
        exit 2
      fi
      ```
      
      **After:**
      ```
      "Verify not writing to credential files (.env, secrets, keys, tokens)"
      ```
      
      ### Pattern: Multiple Conditions → Criteria List
      
      **Before:**
      ```bash
      if [ condition1 ] || [ condition2 ] || [ condition3 ]; then
        echo "Invalid" >&2
        exit 2
      fi
      ```
      
      **After:**
      ```
      "Check: 1) condition1 2) condition2 3) condition3. Deny if any fail."
      ```
      
      ## Conclusion
      
      Migrating to prompt-based hooks makes plugins more maintainable, flexible, and powerful. Reserve command hooks for deterministic checks and external tool integration.
      
    • patterns.md 7 KB
      # Common Hook Patterns
      
      This reference provides common, proven patterns for implementing Claude Code hooks. Use these patterns as starting points for typical hook use cases.
      
      ## Pattern 1: Security Validation
      
      Block dangerous file writes using prompt-based hooks:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Write|Edit",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "File path: $TOOL_INPUT.file_path. Verify: 1) Not in /etc or system directories 2) Not .env or credentials 3) Path doesn't contain '..' traversal. Return 'approve' or 'deny'."
              }
            ]
          }
        ]
      }
      ```
      
      **Use for:** Preventing writes to sensitive files or system directories.
      
      ## Pattern 2: Test Enforcement
      
      Ensure tests run before stopping:
      
      ```json
      {
        "Stop": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Review transcript. If code was modified (Write/Edit tools used), verify tests were executed. If no tests were run, block with reason 'Tests must be run after code changes'."
              }
            ]
          }
        ]
      }
      ```
      
      **Use for:** Enforcing quality standards and preventing incomplete work.
      
      ## Pattern 3: Context Loading
      
      Load project-specific context at session start:
      
      ```json
      {
        "SessionStart": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "command",
                "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
              }
            ]
          }
        ]
      }
      ```
      
      **Example script (load-context.sh):**
      ```bash
      #!/bin/bash
      cd "$CLAUDE_PROJECT_DIR" || exit 1
      
      # Detect project type
      if [ -f "package.json" ]; then
        echo "📦 Node.js project detected"
        echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
      elif [ -f "Cargo.toml" ]; then
        echo "🦀 Rust project detected"
        echo "export PROJECT_TYPE=rust" >> "$CLAUDE_ENV_FILE"
      fi
      ```
      
      **Use for:** Automatically detecting and configuring project-specific settings.
      
      ## Pattern 4: Notification Logging
      
      Log all notifications for audit or analysis:
      
      ```json
      {
        "Notification": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "command",
                "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/log-notification.sh"
              }
            ]
          }
        ]
      }
      ```
      
      **Use for:** Tracking user notifications or integration with external logging systems.
      
      ## Pattern 5: MCP Tool Monitoring
      
      Monitor and validate MCP tool usage:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "mcp__.*__delete.*",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Deletion operation detected. Verify: Is this deletion intentional? Can it be undone? Are there backups? Return 'approve' only if safe."
              }
            ]
          }
        ]
      }
      ```
      
      **Use for:** Protecting against destructive MCP operations.
      
      ## Pattern 6: Build Verification
      
      Ensure project builds after code changes:
      
      ```json
      {
        "Stop": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Check if code was modified. If Write/Edit tools were used, verify the project was built (npm run build, cargo build, etc). If not built, block and request build."
              }
            ]
          }
        ]
      }
      ```
      
      **Use for:** Catching build errors before committing or stopping work.
      
      ## Pattern 7: Permission Confirmation
      
      Ask user before dangerous operations:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Command: $TOOL_INPUT.command. If command contains 'rm', 'delete', 'drop', or other destructive operations, return 'ask' to confirm with user. Otherwise 'approve'."
              }
            ]
          }
        ]
      }
      ```
      
      **Use for:** User confirmation on potentially destructive commands.
      
      ## Pattern 8: Code Quality Checks
      
      Run linters or formatters on file edits:
      
      ```json
      {
        "PostToolUse": [
          {
            "matcher": "Write|Edit",
            "hooks": [
              {
                "type": "command",
                "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/check-quality.sh"
              }
            ]
          }
        ]
      }
      ```
      
      **Example script (check-quality.sh):**
      ```bash
      #!/bin/bash
      input=$(cat)
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      
      # Run linter if applicable
      if [[ "$file_path" == *.js ]] || [[ "$file_path" == *.ts ]]; then
        npx eslint "$file_path" 2>&1 || true
      fi
      ```
      
      **Use for:** Automatic code quality enforcement.
      
      ## Pattern Combinations
      
      Combine multiple patterns for comprehensive protection:
      
      ```json
      {
        "PreToolUse": [
          {
            "matcher": "Write|Edit",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Validate file write safety"
              }
            ]
          },
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Validate bash command safety"
              }
            ]
          }
        ],
        "Stop": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "prompt",
                "prompt": "Verify tests run and build succeeded"
              }
            ]
          }
        ],
        "SessionStart": [
          {
            "matcher": "*",
            "hooks": [
              {
                "type": "command",
                "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
              }
            ]
          }
        ]
      }
      ```
      
      This provides multi-layered protection and automation.
      
      ## Pattern 9: Temporarily Active Hooks
      
      Create hooks that only run when explicitly enabled via flag files:
      
      ```bash
      #!/bin/bash
      # Hook only active when flag file exists
      FLAG_FILE="$CLAUDE_PROJECT_DIR/.enable-security-scan"
      
      if [ ! -f "$FLAG_FILE" ]; then
        # Quick exit when disabled
        exit 0
      fi
      
      # Flag present, run validation
      input=$(cat)
      file_path=$(echo "$input" | jq -r '.tool_input.file_path')
      
      # Run security scan
      security-scanner "$file_path"
      ```
      
      **Activation:**
      ```bash
      # Enable the hook
      touch .enable-security-scan
      
      # Disable the hook
      rm .enable-security-scan
      ```
      
      **Use for:**
      - Temporary debugging hooks
      - Feature flags for development
      - Project-specific validation that's opt-in
      - Performance-intensive checks only when needed
      
      **Note:** Must restart Claude Code after creating/removing flag files for hooks to recognize changes.
      
      ## Pattern 10: Configuration-Driven Hooks
      
      Use JSON configuration to control hook behavior:
      
      ```bash
      #!/bin/bash
      CONFIG_FILE="$CLAUDE_PROJECT_DIR/.claude/my-plugin.local.json"
      
      # Read configuration
      if [ -f "$CONFIG_FILE" ]; then
        strict_mode=$(jq -r '.strictMode // false' "$CONFIG_FILE")
        max_file_size=$(jq -r '.maxFileSize // 1000000' "$CONFIG_FILE")
      else
        # Defaults
        strict_mode=false
        max_file_size=1000000
      fi
      
      # Skip if not in strict mode
      if [ "$strict_mode" != "true" ]; then
        exit 0
      fi
      
      # Apply configured limits
      input=$(cat)
      file_size=$(echo "$input" | jq -r '.tool_input.content | length')
      
      if [ "$file_size" -gt "$max_file_size" ]; then
        echo '{"decision": "deny", "reason": "File exceeds configured size limit"}' >&2
        exit 2
      fi
      ```
      
      **Configuration file (.claude/my-plugin.local.json):**
      ```json
      {
        "strictMode": true,
        "maxFileSize": 500000,
        "allowedPaths": ["/tmp", "/home/user/projects"]
      }
      ```
      
      **Use for:**
      - User-configurable hook behavior
      - Per-project settings
      - Team-specific rules
      - Dynamic validation criteria
      
  • scripts
    • hook-linter.sh 4.1 KB
      #!/bin/bash
      # Hook Linter
      # Checks hook scripts for common issues and best practices
      
      set -euo pipefail
      
      # Usage
      if [ $# -eq 0 ]; then
        echo "Usage: $0 <hook-script.sh> [hook-script2.sh ...]"
        echo ""
        echo "Checks hook scripts for:"
        echo "  - Shebang presence"
        echo "  - set -euo pipefail usage"
        echo "  - Input reading from stdin"
        echo "  - Proper error handling"
        echo "  - Variable quoting"
        echo "  - Exit code usage"
        echo "  - Hardcoded paths"
        echo "  - Timeout considerations"
        exit 1
      fi
      
      check_script() {
        local script="$1"
        local warnings=0
        local errors=0
      
        echo "🔍 Linting: $script"
        echo ""
      
        if [ ! -f "$script" ]; then
          echo "❌ Error: File not found"
          return 1
        fi
      
        # Check 1: Executable
        if [ ! -x "$script" ]; then
          echo "⚠️  Not executable (chmod +x $script)"
          ((warnings++))
        fi
      
        # Check 2: Shebang
        first_line=$(head -1 "$script")
        if [[ ! "$first_line" =~ ^#!/ ]]; then
          echo "❌ Missing shebang (#!/bin/bash)"
          ((errors++))
        fi
      
        # Check 3: set -euo pipefail
        if ! grep -q "set -euo pipefail" "$script"; then
          echo "⚠️  Missing 'set -euo pipefail' (recommended for safety)"
          ((warnings++))
        fi
      
        # Check 4: Reads from stdin
        if ! grep -q "cat\|read" "$script"; then
          echo "⚠️  Doesn't appear to read input from stdin"
          ((warnings++))
        fi
      
        # Check 5: Uses jq for JSON parsing
        if grep -q "tool_input\|tool_name" "$script" && ! grep -q "jq" "$script"; then
          echo "⚠️  Parses hook input but doesn't use jq"
          ((warnings++))
        fi
      
        # Check 6: Unquoted variables
        if grep -E '\$[A-Za-z_][A-Za-z0-9_]*[^"]' "$script" | grep -v '#' | grep -q .; then
          echo "⚠️  Potentially unquoted variables detected (injection risk)"
          echo "   Always use double quotes: \"\$variable\" not \$variable"
          ((warnings++))
        fi
      
        # Check 7: Hardcoded paths
        if grep -E '^[^#]*/home/|^[^#]*/usr/|^[^#]*/opt/' "$script" | grep -q .; then
          echo "⚠️  Hardcoded absolute paths detected"
          echo "   Use \$CLAUDE_PROJECT_DIR or \$CLAUDE_PLUGIN_ROOT"
          ((warnings++))
        fi
      
        # Check 8: Uses CLAUDE_PLUGIN_ROOT
        if ! grep -q "CLAUDE_PLUGIN_ROOT\|CLAUDE_PROJECT_DIR" "$script"; then
          echo "💡 Tip: Use \$CLAUDE_PLUGIN_ROOT for plugin-relative paths"
        fi
      
        # Check 9: Exit codes
        if ! grep -q "exit 0\|exit 2" "$script"; then
          echo "⚠️  No explicit exit codes (should exit 0 or 2)"
          ((warnings++))
        fi
      
        # Check 10: JSON output for decision hooks
        if grep -q "PreToolUse\|Stop" "$script"; then
          if ! grep -q "permissionDecision\|decision" "$script"; then
            echo "💡 Tip: PreToolUse/Stop hooks should output decision JSON"
          fi
        fi
      
        # Check 11: Long-running commands
        if grep -E 'sleep [0-9]{3,}|while true' "$script" | grep -v '#' | grep -q .; then
          echo "⚠️  Potentially long-running code detected"
          echo "   Hooks should complete quickly (< 60s)"
          ((warnings++))
        fi
      
        # Check 12: Error messages to stderr
        if grep -q 'echo.*".*error\|Error\|denied\|Denied' "$script"; then
          if ! grep -q '>&2' "$script"; then
            echo "⚠️  Error messages should be written to stderr (>&2)"
            ((warnings++))
          fi
        fi
      
        # Check 13: Input validation
        if ! grep -q "if.*empty\|if.*null\|if.*-z" "$script"; then
          echo "💡 Tip: Consider validating input fields aren't empty"
        fi
      
        echo ""
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      
        if [ $errors -eq 0 ] && [ $warnings -eq 0 ]; then
          echo "✅ No issues found"
          return 0
        elif [ $errors -eq 0 ]; then
          echo "⚠️  Found $warnings warning(s)"
          return 0
        else
          echo "❌ Found $errors error(s) and $warnings warning(s)"
          return 1
        fi
      }
      
      echo "🔎 Hook Script Linter"
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo ""
      
      total_errors=0
      
      for script in "$@"; do
        if ! check_script "$script"; then
          ((total_errors++))
        fi
        echo ""
      done
      
      if [ $total_errors -eq 0 ]; then
        echo "✅ All scripts passed linting"
        exit 0
      else
        echo "❌ $total_errors script(s) had errors"
        exit 1
      fi
      
    • test-hook.sh 5.2 KB
      #!/bin/bash
      # Hook Testing Helper
      # Tests a hook with sample input and shows output
      
      set -euo pipefail
      
      # Usage
      show_usage() {
        echo "Usage: $0 [options] <hook-script> <test-input.json>"
        echo ""
        echo "Options:"
        echo "  -h, --help      Show this help message"
        echo "  -v, --verbose   Show detailed execution information"
        echo "  -t, --timeout N Set timeout in seconds (default: 60)"
        echo ""
        echo "Examples:"
        echo "  $0 validate-bash.sh test-input.json"
        echo "  $0 -v -t 30 validate-write.sh write-input.json"
        echo ""
        echo "Creates sample test input with:"
        echo "  $0 --create-sample <event-type>"
        exit 0
      }
      
      # Create sample input
      create_sample() {
        event_type="$1"
      
        case "$event_type" in
          PreToolUse)
            cat <<'EOF'
      {
        "session_id": "test-session",
        "transcript_path": "/tmp/transcript.txt",
        "cwd": "/tmp/test-project",
        "permission_mode": "ask",
        "hook_event_name": "PreToolUse",
        "tool_name": "Write",
        "tool_input": {
          "file_path": "/tmp/test.txt",
          "content": "Test content"
        }
      }
      EOF
            ;;
          PostToolUse)
            cat <<'EOF'
      {
        "session_id": "test-session",
        "transcript_path": "/tmp/transcript.txt",
        "cwd": "/tmp/test-project",
        "permission_mode": "ask",
        "hook_event_name": "PostToolUse",
        "tool_name": "Bash",
        "tool_result": "Command executed successfully"
      }
      EOF
            ;;
          Stop|SubagentStop)
            cat <<'EOF'
      {
        "session_id": "test-session",
        "transcript_path": "/tmp/transcript.txt",
        "cwd": "/tmp/test-project",
        "permission_mode": "ask",
        "hook_event_name": "Stop",
        "reason": "Task appears complete"
      }
      EOF
            ;;
          UserPromptSubmit)
            cat <<'EOF'
      {
        "session_id": "test-session",
        "transcript_path": "/tmp/transcript.txt",
        "cwd": "/tmp/test-project",
        "permission_mode": "ask",
        "hook_event_name": "UserPromptSubmit",
        "user_prompt": "Test user prompt"
      }
      EOF
            ;;
          SessionStart|SessionEnd)
            cat <<'EOF'
      {
        "session_id": "test-session",
        "transcript_path": "/tmp/transcript.txt",
        "cwd": "/tmp/test-project",
        "permission_mode": "ask",
        "hook_event_name": "SessionStart"
      }
      EOF
            ;;
          *)
            echo "Unknown event type: $event_type"
            echo "Valid types: PreToolUse, PostToolUse, Stop, SubagentStop, UserPromptSubmit, SessionStart, SessionEnd"
            exit 1
            ;;
        esac
      }
      
      # Parse arguments
      VERBOSE=false
      TIMEOUT=60
      
      while [ $# -gt 0 ]; do
        case "$1" in
          -h|--help)
            show_usage
            ;;
          -v|--verbose)
            VERBOSE=true
            shift
            ;;
          -t|--timeout)
            TIMEOUT="$2"
            shift 2
            ;;
          --create-sample)
            create_sample "$2"
            exit 0
            ;;
          *)
            break
            ;;
        esac
      done
      
      if [ $# -ne 2 ]; then
        echo "Error: Missing required arguments"
        echo ""
        show_usage
      fi
      
      HOOK_SCRIPT="$1"
      TEST_INPUT="$2"
      
      # Validate inputs
      if [ ! -f "$HOOK_SCRIPT" ]; then
        echo "❌ Error: Hook script not found: $HOOK_SCRIPT"
        exit 1
      fi
      
      if [ ! -x "$HOOK_SCRIPT" ]; then
        echo "⚠️  Warning: Hook script is not executable. Attempting to run with bash..."
        HOOK_SCRIPT="bash $HOOK_SCRIPT"
      fi
      
      if [ ! -f "$TEST_INPUT" ]; then
        echo "❌ Error: Test input not found: $TEST_INPUT"
        exit 1
      fi
      
      # Validate test input JSON
      if ! jq empty "$TEST_INPUT" 2>/dev/null; then
        echo "❌ Error: Test input is not valid JSON"
        exit 1
      fi
      
      echo "🧪 Testing hook: $HOOK_SCRIPT"
      echo "📥 Input: $TEST_INPUT"
      echo ""
      
      if [ "$VERBOSE" = true ]; then
        echo "Input JSON:"
        jq . "$TEST_INPUT"
        echo ""
      fi
      
      # Set up environment
      export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/tmp/test-project}"
      export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(pwd)}"
      export CLAUDE_ENV_FILE="${CLAUDE_ENV_FILE:-/tmp/test-env-$$}"
      
      if [ "$VERBOSE" = true ]; then
        echo "Environment:"
        echo "  CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR"
        echo "  CLAUDE_PLUGIN_ROOT=$CLAUDE_PLUGIN_ROOT"
        echo "  CLAUDE_ENV_FILE=$CLAUDE_ENV_FILE"
        echo ""
      fi
      
      # Run the hook
      echo "▶️  Running hook (timeout: ${TIMEOUT}s)..."
      echo ""
      
      start_time=$(date +%s)
      
      set +e
      output=$(timeout "$TIMEOUT" bash -c "cat '$TEST_INPUT' | $HOOK_SCRIPT" 2>&1)
      exit_code=$?
      set -e
      
      end_time=$(date +%s)
      duration=$((end_time - start_time))
      
      # Analyze results
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo "Results:"
      echo ""
      echo "Exit Code: $exit_code"
      echo "Duration: ${duration}s"
      echo ""
      
      case $exit_code in
        0)
          echo "✅ Hook approved/succeeded"
          ;;
        2)
          echo "🚫 Hook blocked/denied"
          ;;
        124)
          echo "⏱️  Hook timed out after ${TIMEOUT}s"
          ;;
        *)
          echo "⚠️  Hook returned unexpected exit code: $exit_code"
          ;;
      esac
      
      echo ""
      echo "Output:"
      if [ -n "$output" ]; then
        echo "$output"
        echo ""
      
        # Try to parse as JSON
        if echo "$output" | jq empty 2>/dev/null; then
          echo "Parsed JSON output:"
          echo "$output" | jq .
        fi
      else
        echo "(no output)"
      fi
      
      # Check for environment file
      if [ -f "$CLAUDE_ENV_FILE" ]; then
        echo ""
        echo "Environment file created:"
        cat "$CLAUDE_ENV_FILE"
        rm -f "$CLAUDE_ENV_FILE"
      fi
      
      echo ""
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      
      if [ $exit_code -eq 0 ] || [ $exit_code -eq 2 ]; then
        echo "✅ Test completed successfully"
        exit 0
      else
        echo "❌ Test failed"
        exit 1
      fi
      
    • validate-hook-schema.sh 5 KB
      #!/bin/bash
      # Hook Schema Validator
      # Validates hooks.json structure and checks for common issues
      
      set -euo pipefail
      
      # Usage
      if [ $# -eq 0 ]; then
        echo "Usage: $0 <path/to/hooks.json>"
        echo ""
        echo "Validates hook configuration file for:"
        echo "  - Valid JSON syntax"
        echo "  - Required fields"
        echo "  - Hook type validity"
        echo "  - Matcher patterns"
        echo "  - Timeout ranges"
        exit 1
      fi
      
      HOOKS_FILE="$1"
      
      if [ ! -f "$HOOKS_FILE" ]; then
        echo "❌ Error: File not found: $HOOKS_FILE"
        exit 1
      fi
      
      echo "🔍 Validating hooks configuration: $HOOKS_FILE"
      echo ""
      
      # Check 1: Valid JSON
      echo "Checking JSON syntax..."
      if ! jq empty "$HOOKS_FILE" 2>/dev/null; then
        echo "❌ Invalid JSON syntax"
        exit 1
      fi
      echo "✅ Valid JSON"
      
      # Check 2: Root structure
      echo ""
      echo "Checking root structure..."
      VALID_EVENTS=("PreToolUse" "PostToolUse" "UserPromptSubmit" "Stop" "SubagentStop" "SessionStart" "SessionEnd" "PreCompact" "Notification")
      
      for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
        found=false
        for valid_event in "${VALID_EVENTS[@]}"; do
          if [ "$event" = "$valid_event" ]; then
            found=true
            break
          fi
        done
      
        if [ "$found" = false ]; then
          echo "⚠️  Unknown event type: $event"
        fi
      done
      echo "✅ Root structure valid"
      
      # Check 3: Validate each hook
      echo ""
      echo "Validating individual hooks..."
      
      error_count=0
      warning_count=0
      
      for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
        hook_count=$(jq -r ".\"$event\" | length" "$HOOKS_FILE")
      
        for ((i=0; i<hook_count; i++)); do
          # Check matcher exists
          matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
          if [ -z "$matcher" ]; then
            echo "❌ $event[$i]: Missing 'matcher' field"
            ((error_count++))
            continue
          fi
      
          # Check hooks array exists
          hooks=$(jq -r ".\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
          if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then
            echo "❌ $event[$i]: Missing 'hooks' array"
            ((error_count++))
            continue
          fi
      
          # Validate each hook in the array
          hook_array_count=$(jq -r ".\"$event\"[$i].hooks | length" "$HOOKS_FILE")
      
          for ((j=0; j<hook_array_count; j++)); do
            hook_type=$(jq -r ".\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
      
            if [ -z "$hook_type" ]; then
              echo "❌ $event[$i].hooks[$j]: Missing 'type' field"
              ((error_count++))
              continue
            fi
      
            if [ "$hook_type" != "command" ] && [ "$hook_type" != "prompt" ]; then
              echo "❌ $event[$i].hooks[$j]: Invalid type '$hook_type' (must be 'command' or 'prompt')"
              ((error_count++))
              continue
            fi
      
            # Check type-specific fields
            if [ "$hook_type" = "command" ]; then
              command=$(jq -r ".\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
              if [ -z "$command" ]; then
                echo "❌ $event[$i].hooks[$j]: Command hooks must have 'command' field"
                ((error_count++))
              else
                # Check for hardcoded paths
                if [[ "$command" == /* ]] && [[ "$command" != *'${CLAUDE_PLUGIN_ROOT}'* ]]; then
                  echo "⚠️  $event[$i].hooks[$j]: Hardcoded absolute path detected. Consider using \${CLAUDE_PLUGIN_ROOT}"
                  ((warning_count++))
                fi
              fi
            elif [ "$hook_type" = "prompt" ]; then
              prompt=$(jq -r ".\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
              if [ -z "$prompt" ]; then
                echo "❌ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field"
                ((error_count++))
              fi
      
              # Check if prompt-based hooks are used on supported events
              if [ "$event" != "Stop" ] && [ "$event" != "SubagentStop" ] && [ "$event" != "UserPromptSubmit" ] && [ "$event" != "PreToolUse" ]; then
                echo "⚠️  $event[$i].hooks[$j]: Prompt hooks may not be fully supported on $event (best on Stop, SubagentStop, UserPromptSubmit, PreToolUse)"
                ((warning_count++))
              fi
            fi
      
            # Check timeout
            timeout=$(jq -r ".\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
            if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then
              if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
                echo "❌ $event[$i].hooks[$j]: Timeout must be a number"
                ((error_count++))
              elif [ "$timeout" -gt 600 ]; then
                echo "⚠️  $event[$i].hooks[$j]: Timeout $timeout seconds is very high (max 600s)"
                ((warning_count++))
              elif [ "$timeout" -lt 5 ]; then
                echo "⚠️  $event[$i].hooks[$j]: Timeout $timeout seconds is very low"
                ((warning_count++))
              fi
            fi
          done
        done
      done
      
      echo ""
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
        echo "✅ All checks passed!"
        exit 0
      elif [ $error_count -eq 0 ]; then
        echo "⚠️  Validation passed with $warning_count warning(s)"
        exit 0
      else
        echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)"
        exit 1
      fi
      
  • SKILL.md 15.9 KB
    ---
    name: Hook Development
    description: This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.
    version: 0.1.0
    ---
    
    # Hook Development for Claude Code Plugins
    
    ## Overview
    
    Hooks are event-driven automation scripts that execute in response to Claude Code events. Use hooks to validate operations, enforce policies, add context, and integrate external tools into workflows.
    
    **Key capabilities:**
    - Validate tool calls before execution (PreToolUse)
    - React to tool results (PostToolUse)
    - Enforce completion standards (Stop, SubagentStop)
    - Load project context (SessionStart)
    - Automate workflows across the development lifecycle
    
    ## Hook Types
    
    ### Prompt-Based Hooks (Recommended)
    
    Use LLM-driven decision making for context-aware validation:
    
    ```json
    {
      "type": "prompt",
      "prompt": "Evaluate if this tool use is appropriate: $TOOL_INPUT",
      "timeout": 30
    }
    ```
    
    **Supported events:** Stop, SubagentStop, UserPromptSubmit, PreToolUse
    
    **Benefits:**
    - Context-aware decisions based on natural language reasoning
    - Flexible evaluation logic without bash scripting
    - Better edge case handling
    - Easier to maintain and extend
    
    ### Command Hooks
    
    Execute bash commands for deterministic checks:
    
    ```json
    {
      "type": "command",
      "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
      "timeout": 60
    }
    ```
    
    **Use for:**
    - Fast deterministic validations
    - File system operations
    - External tool integrations
    - Performance-critical checks
    
    ## Hook Configuration Formats
    
    ### Plugin hooks.json Format
    
    **For plugin hooks** in `hooks/hooks.json`, use wrapper format:
    
    ```json
    {
      "description": "Brief explanation of hooks (optional)",
      "hooks": {
        "PreToolUse": [...],
        "Stop": [...],
        "SessionStart": [...]
      }
    }
    ```
    
    **Key points:**
    - `description` field is optional
    - `hooks` field is required wrapper containing actual hook events
    - This is the **plugin-specific format**
    
    **Example:**
    ```json
    {
      "description": "Validation hooks for code quality",
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Write",
            "hooks": [
              {
                "type": "command",
                "command": "${CLAUDE_PLUGIN_ROOT}/hooks/validate.sh"
              }
            ]
          }
        ]
      }
    }
    ```
    
    ### Settings Format (Direct)
    
    **For user settings** in `.claude/settings.json`, use direct format:
    
    ```json
    {
      "PreToolUse": [...],
      "Stop": [...],
      "SessionStart": [...]
    }
    ```
    
    **Key points:**
    - No wrapper - events directly at top level
    - No description field
    - This is the **settings format**
    
    **Important:** The examples below show the hook event structure that goes inside either format. For plugin hooks.json, wrap these in `{"hooks": {...}}`.
    
    ## Hook Events
    
    ### PreToolUse
    
    Execute before any tool runs. Use to approve, deny, or modify tool calls.
    
    **Example (prompt-based):**
    ```json
    {
      "PreToolUse": [
        {
          "matcher": "Write|Edit",
          "hooks": [
            {
              "type": "prompt",
              "prompt": "Validate file write safety. Check: system paths, credentials, path traversal, sensitive content. Return 'approve' or 'deny'."
            }
          ]
        }
      ]
    }
    ```
    
    **Output for PreToolUse:**
    ```json
    {
      "hookSpecificOutput": {
        "permissionDecision": "allow|deny|ask",
        "updatedInput": {"field": "modified_value"}
      },
      "systemMessage": "Explanation for Claude"
    }
    ```
    
    ### PostToolUse
    
    Execute after tool completes. Use to react to results, provide feedback, or log.
    
    **Example:**
    ```json
    {
      "PostToolUse": [
        {
          "matcher": "Edit",
          "hooks": [
            {
              "type": "prompt",
              "prompt": "Analyze edit result for potential issues: syntax errors, security vulnerabilities, breaking changes. Provide feedback."
            }
          ]
        }
      ]
    }
    ```
    
    **Output behavior:**
    - Exit 0: stdout shown in transcript
    - Exit 2: stderr fed back to Claude
    - systemMessage included in context
    
    ### Stop
    
    Execute when main agent considers stopping. Use to validate completeness.
    
    **Example:**
    ```json
    {
      "Stop": [
        {
          "matcher": "*",
          "hooks": [
            {
              "type": "prompt",
              "prompt": "Verify task completion: tests run, build succeeded, questions answered. Return 'approve' to stop or 'block' with reason to continue."
            }
          ]
        }
      ]
    }
    ```
    
    **Decision output:**
    ```json
    {
      "decision": "approve|block",
      "reason": "Explanation",
      "systemMessage": "Additional context"
    }
    ```
    
    ### SubagentStop
    
    Execute when subagent considers stopping. Use to ensure subagent completed its task.
    
    Similar to Stop hook, but for subagents.
    
    ### UserPromptSubmit
    
    Execute when user submits a prompt. Use to add context, validate, or block prompts.
    
    **Example:**
    ```json
    {
      "UserPromptSubmit": [
        {
          "matcher": "*",
          "hooks": [
            {
              "type": "prompt",
              "prompt": "Check if prompt requires security guidance. If discussing auth, permissions, or API security, return relevant warnings."
            }
          ]
        }
      ]
    }
    ```
    
    ### SessionStart
    
    Execute when Claude Code session begins. Use to load context and set environment.
    
    **Example:**
    ```json
    {
      "SessionStart": [
        {
          "matcher": "*",
          "hooks": [
            {
              "type": "command",
              "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh"
            }
          ]
        }
      ]
    }
    ```
    
    **Special capability:** Persist environment variables using `$CLAUDE_ENV_FILE`:
    ```bash
    echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
    ```
    
    See `examples/load-context.sh` for complete example.
    
    ### SessionEnd
    
    Execute when session ends. Use for cleanup, logging, and state preservation.
    
    ### PreCompact
    
    Execute before context compaction. Use to add critical information to preserve.
    
    ### Notification
    
    Execute when Claude sends notifications. Use to react to user notifications.
    
    ## Hook Output Format
    
    ### Standard Output (All Hooks)
    
    ```json
    {
      "continue": true,
      "suppressOutput": false,
      "systemMessage": "Message for Claude"
    }
    ```
    
    - `continue`: If false, halt processing (default true)
    - `suppressOutput`: Hide output from transcript (default false)
    - `systemMessage`: Message shown to Claude
    
    ### Exit Codes
    
    - `0` - Success (stdout shown in transcript)
    - `2` - Blocking error (stderr fed back to Claude)
    - Other - Non-blocking error
    
    ## Hook Input Format
    
    All hooks receive JSON via stdin with common fields:
    
    ```json
    {
      "session_id": "abc123",
      "transcript_path": "/path/to/transcript.txt",
      "cwd": "/current/working/dir",
      "permission_mode": "ask|allow",
      "hook_event_name": "PreToolUse"
    }
    ```
    
    **Event-specific fields:**
    
    - **PreToolUse/PostToolUse:** `tool_name`, `tool_input`, `tool_result`
    - **UserPromptSubmit:** `user_prompt`
    - **Stop/SubagentStop:** `reason`
    
    Access fields in prompts using `$TOOL_INPUT`, `$TOOL_RESULT`, `$USER_PROMPT`, etc.
    
    ## Environment Variables
    
    Available in all command hooks:
    
    - `$CLAUDE_PROJECT_DIR` - Project root path
    - `$CLAUDE_PLUGIN_ROOT` - Plugin directory (use for portable paths)
    - `$CLAUDE_ENV_FILE` - SessionStart only: persist env vars here
    - `$CLAUDE_CODE_REMOTE` - Set if running in remote context
    
    **Always use ${CLAUDE_PLUGIN_ROOT} in hook commands for portability:**
    
    ```json
    {
      "type": "command",
      "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
    }
    ```
    
    ## Plugin Hook Configuration
    
    In plugins, define hooks in `hooks/hooks.json`:
    
    ```json
    {
      "PreToolUse": [
        {
          "matcher": "Write|Edit",
          "hooks": [
            {
              "type": "prompt",
              "prompt": "Validate file write safety"
            }
          ]
        }
      ],
      "Stop": [
        {
          "matcher": "*",
          "hooks": [
            {
              "type": "prompt",
              "prompt": "Verify task completion"
            }
          ]
        }
      ],
      "SessionStart": [
        {
          "matcher": "*",
          "hooks": [
            {
              "type": "command",
              "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh",
              "timeout": 10
            }
          ]
        }
      ]
    }
    ```
    
    Plugin hooks merge with user's hooks and run in parallel.
    
    ## Matchers
    
    ### Tool Name Matching
    
    **Exact match:**
    ```json
    "matcher": "Write"
    ```
    
    **Multiple tools:**
    ```json
    "matcher": "Read|Write|Edit"
    ```
    
    **Wildcard (all tools):**
    ```json
    "matcher": "*"
    ```
    
    **Regex patterns:**
    ```json
    "matcher": "mcp__.*__delete.*"  // All MCP delete tools
    ```
    
    **Note:** Matchers are case-sensitive.
    
    ### Common Patterns
    
    ```json
    // All MCP tools
    "matcher": "mcp__.*"
    
    // Specific plugin's MCP tools
    "matcher": "mcp__plugin_asana_.*"
    
    // All file operations
    "matcher": "Read|Write|Edit"
    
    // Bash commands only
    "matcher": "Bash"
    ```
    
    ## Security Best Practices
    
    ### Input Validation
    
    Always validate inputs in command hooks:
    
    ```bash
    #!/bin/bash
    set -euo pipefail
    
    input=$(cat)
    tool_name=$(echo "$input" | jq -r '.tool_name')
    
    # Validate tool name format
    if [[ ! "$tool_name" =~ ^[a-zA-Z0-9_]+$ ]]; then
      echo '{"decision": "deny", "reason": "Invalid tool name"}' >&2
      exit 2
    fi
    ```
    
    ### Path Safety
    
    Check for path traversal and sensitive files:
    
    ```bash
    file_path=$(echo "$input" | jq -r '.tool_input.file_path')
    
    # Deny path traversal
    if [[ "$file_path" == *".."* ]]; then
      echo '{"decision": "deny", "reason": "Path traversal detected"}' >&2
      exit 2
    fi
    
    # Deny sensitive files
    if [[ "$file_path" == *".env"* ]]; then
      echo '{"decision": "deny", "reason": "Sensitive file"}' >&2
      exit 2
    fi
    ```
    
    See `examples/validate-write.sh` and `examples/validate-bash.sh` for complete examples.
    
    ### Quote All Variables
    
    ```bash
    # GOOD: Quoted
    echo "$file_path"
    cd "$CLAUDE_PROJECT_DIR"
    
    # BAD: Unquoted (injection risk)
    echo $file_path
    cd $CLAUDE_PROJECT_DIR
    ```
    
    ### Set Appropriate Timeouts
    
    ```json
    {
      "type": "command",
      "command": "bash script.sh",
      "timeout": 10
    }
    ```
    
    **Defaults:** Command hooks (60s), Prompt hooks (30s)
    
    ## Performance Considerations
    
    ### Parallel Execution
    
    All matching hooks run **in parallel**:
    
    ```json
    {
      "PreToolUse": [
        {
          "matcher": "Write",
          "hooks": [
            {"type": "command", "command": "check1.sh"},  // Parallel
            {"type": "command", "command": "check2.sh"},  // Parallel
            {"type": "prompt", "prompt": "Validate..."}   // Parallel
          ]
        }
      ]
    }
    ```
    
    **Design implications:**
    - Hooks don't see each other's output
    - Non-deterministic ordering
    - Design for independence
    
    ### Optimization
    
    1. Use command hooks for quick deterministic checks
    2. Use prompt hooks for complex reasoning
    3. Cache validation results in temp files
    4. Minimize I/O in hot paths
    
    ## Temporarily Active Hooks
    
    Create hooks that activate conditionally by checking for a flag file or configuration:
    
    **Pattern: Flag file activation**
    ```bash
    #!/bin/bash
    # Only active when flag file exists
    FLAG_FILE="$CLAUDE_PROJECT_DIR/.enable-strict-validation"
    
    if [ ! -f "$FLAG_FILE" ]; then
      # Flag not present, skip validation
      exit 0
    fi
    
    # Flag present, run validation
    input=$(cat)
    # ... validation logic ...
    ```
    
    **Pattern: Configuration-based activation**
    ```bash
    #!/bin/bash
    # Check configuration for activation
    CONFIG_FILE="$CLAUDE_PROJECT_DIR/.claude/plugin-config.json"
    
    if [ -f "$CONFIG_FILE" ]; then
      enabled=$(jq -r '.strictMode // false' "$CONFIG_FILE")
      if [ "$enabled" != "true" ]; then
        exit 0  # Not enabled, skip
      fi
    fi
    
    # Enabled, run hook logic
    input=$(cat)
    # ... hook logic ...
    ```
    
    **Use cases:**
    - Enable strict validation only when needed
    - Temporary debugging hooks
    - Project-specific hook behavior
    - Feature flags for hooks
    
    **Best practice:** Document activation mechanism in plugin README so users know how to enable/disable temporary hooks.
    
    ## Hook Lifecycle and Limitations
    
    ### Hooks Load at Session Start
    
    **Important:** Hooks are loaded when Claude Code session starts. Changes to hook configuration require restarting Claude Code.
    
    **Cannot hot-swap hooks:**
    - Editing `hooks/hooks.json` won't affect current session
    - Adding new hook scripts won't be recognized
    - Changing hook commands/prompts won't update
    - Must restart Claude Code: exit and run `claude` again
    
    **To test hook changes:**
    1. Edit hook configuration or scripts
    2. Exit Claude Code session
    3. Restart: `claude` or `cc`
    4. New hook configuration loads
    5. Test hooks with `claude --debug`
    
    ### Hook Validation at Startup
    
    Hooks are validated when Claude Code starts:
    - Invalid JSON in hooks.json causes loading failure
    - Missing scripts cause warnings
    - Syntax errors reported in debug mode
    
    Use `/hooks` command to review loaded hooks in current session.
    
    ## Debugging Hooks
    
    ### Enable Debug Mode
    
    ```bash
    claude --debug
    ```
    
    Look for hook registration, execution logs, input/output JSON, and timing information.
    
    ### Test Hook Scripts
    
    Test command hooks directly:
    
    ```bash
    echo '{"tool_name": "Write", "tool_input": {"file_path": "/test"}}' | \
      bash ${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh
    
    echo "Exit code: $?"
    ```
    
    ### Validate JSON Output
    
    Ensure hooks output valid JSON:
    
    ```bash
    output=$(./your-hook.sh < test-input.json)
    echo "$output" | jq .
    ```
    
    ## Quick Reference
    
    ### Hook Events Summary
    
    | Event | When | Use For |
    |-------|------|---------|
    | PreToolUse | Before tool | Validation, modification |
    | PostToolUse | After tool | Feedback, logging |
    | UserPromptSubmit | User input | Context, validation |
    | Stop | Agent stopping | Completeness check |
    | SubagentStop | Subagent done | Task validation |
    | SessionStart | Session begins | Context loading |
    | SessionEnd | Session ends | Cleanup, logging |
    | PreCompact | Before compact | Preserve context |
    | Notification | User notified | Logging, reactions |
    
    ### Best Practices
    
    **DO:**
    - ✅ Use prompt-based hooks for complex logic
    - ✅ Use ${CLAUDE_PLUGIN_ROOT} for portability
    - ✅ Validate all inputs in command hooks
    - ✅ Quote all bash variables
    - ✅ Set appropriate timeouts
    - ✅ Return structured JSON output
    - ✅ Test hooks thoroughly
    
    **DON'T:**
    - ❌ Use hardcoded paths
    - ❌ Trust user input without validation
    - ❌ Create long-running hooks
    - ❌ Rely on hook execution order
    - ❌ Modify global state unpredictably
    - ❌ Log sensitive information
    
    ## Additional Resources
    
    ### Reference Files
    
    For detailed patterns and advanced techniques, consult:
    
    - **`references/patterns.md`** - Common hook patterns (8+ proven patterns)
    - **`references/migration.md`** - Migrating from basic to advanced hooks
    - **`references/advanced.md`** - Advanced use cases and techniques
    
    ### Example Hook Scripts
    
    Working examples in `examples/`:
    
    - **`validate-write.sh`** - File write validation example
    - **`validate-bash.sh`** - Bash command validation example
    - **`load-context.sh`** - SessionStart context loading example
    
    ### Utility Scripts
    
    Development tools in `scripts/`:
    
    - **`validate-hook-schema.sh`** - Validate hooks.json structure and syntax
    - **`test-hook.sh`** - Test hooks with sample input before deployment
    - **`hook-linter.sh`** - Check hook scripts for common issues and best practices
    
    ### External Resources
    
    - **Official Docs**: https://docs.claude.com/en/docs/claude-code/hooks
    - **Examples**: See security-guidance plugin in marketplace
    - **Testing**: Use `claude --debug` for detailed logs
    - **Validation**: Use `jq` to validate hook JSON output
    
    ## Implementation Workflow
    
    To implement hooks in a plugin:
    
    1. Identify events to hook into (PreToolUse, Stop, SessionStart, etc.)
    2. Decide between prompt-based (flexible) or command (deterministic) hooks
    3. Write hook configuration in `hooks/hooks.json`
    4. For command hooks, create hook scripts
    5. Use ${CLAUDE_PLUGIN_ROOT} for all file references
    6. Validate configuration with `scripts/validate-hook-schema.sh hooks/hooks.json`
    7. Test hooks with `scripts/test-hook.sh` before deployment
    8. Test in Claude Code with `claude --debug`
    9. Document hooks in plugin README
    
    Focus on prompt-based hooks for most use cases. Reserve command hooks for performance-critical or deterministic checks.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related