Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save hidoos/575960c6f98044e33207c61b0e494479 to your computer and use it in GitHub Desktop.

Select an option

Save hidoos/575960c6f98044e33207c61b0e494479 to your computer and use it in GitHub Desktop.
Claude Code Hook Development Skill - Install to .claude/skills/hook-development/

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:

{
  "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:

#!/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:

#!/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:

#!/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:

# 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
# 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:

#!/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:

{
  "strict_mode": true,
  "allowed_commands": ["ls", "pwd", "grep"],
  "forbidden_paths": ["/etc", "/sys"]
}

Context-Aware Prompt Hooks

Use transcript and session context for intelligent decisions:

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

#!/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:

{
  "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:

#!/bin/bash
# Initialize session tracking
echo "0" > /tmp/test-count-$$
echo "0" > /tmp/build-count-$$

PostToolUse - Track events:

#!/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:

#!/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

#!/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

#!/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

#!/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

#!/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

#!/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

#!/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

# 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:

# 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

# 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

# BAD: Hook takes 2 minutes to run
sleep 120
# This will timeout and block the workflow

❌ Uncaught Exceptions

# 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

# 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.

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:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "bash validate-bash.sh"
        }
      ]
    }
  ]
}

Script (validate-bash.sh):

#!/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:

{
  "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:

{
  "PreToolUse": [
    {
      "matcher": "Write",
      "hooks": [
        {
          "type": "command",
          "command": "bash validate-write.sh"
        }
      ]
    }
  ]
}

Script (validate-write.sh):

#!/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:

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

#!/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

#!/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)

#!/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:

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

{
  "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:

if [[ "$command" == *"sudo"* ]]; then
  echo "Privilege escalation" >&2
  exit 2
fi

After:

"Check for privilege escalation (sudo, su, etc)"

Pattern: Regex → Intent

Before:

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:

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.

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:

{
  "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:

{
  "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:

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

Example script (load-context.sh):

#!/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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "PostToolUse": [
    {
      "matcher": "Write|Edit",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/check-quality.sh"
        }
      ]
    }
  ]
}

Example script (check-quality.sh):

#!/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:

{
  "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:

#!/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:

# 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:

#!/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):

{
  "strictMode": true,
  "maxFileSize": 500000,
  "allowedPaths": ["/tmp", "/home/user/projects"]
}

Use for:

  • User-configurable hook behavior
  • Per-project settings
  • Team-specific rules
  • Dynamic validation criteria

Agent Hooks (v2.1.0)

Agent hooks tie into agent/subagent lifecycles for scoped behaviors.

Configuration

{
  "type": "agent",
  "agent": "my-validator-agent"
}

Supported Events

  • PreToolUse
  • PostToolUse
  • Stop

Key Characteristics

  • Executes within agent context
  • Fully supported from plugins as of v2.1.0 (was command-only before)
  • Useful for complex validation requiring agent reasoning

Use Cases

  • Multi-step validation workflows
  • Context-aware decision making that needs agent capabilities
  • Subagent orchestration patterns

Plugin Support

As of v2.1.0, plugins support all hook types (command, prompt, agent). Previously only command hooks were available from plugins.

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

Frontmatter Integration (v2.1.0)

Hooks can be embedded directly in YAML frontmatter for agents, skills, and slash commands. This scopes the hook to the component's lifecycle - hooks only run during that specific component's execution.

Supported Components

  • Agents
  • Skills
  • Slash Commands

Supported Events

  • PreToolUse
  • PostToolUse
  • Stop

Agent Frontmatter Example

---
name: secure-agent
description: Agent with built-in security checks
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: command
          command: "./scripts/file-protector.sh"
---
Your agent instructions here...

Skill Frontmatter Example

---
name: code-formatter
description: Formats code after changes
hooks:
  PostToolUse:
    - matcher: "Edit"
      hooks:
        - type: prompt
          prompt: "Check if formatting is needed: $ARGUMENTS"
---
Skill prompt content...

Slash Command Frontmatter Example

---
name: deploy
description: Deploy with validation
hooks:
  Stop:
    - matcher: "*"
      hooks:
        - type: prompt
          prompt: "Verify deployment checklist complete"
---
Command instructions...

Benefits

  • Hooks scoped to component lifecycle (no global interference)
  • Self-contained component definitions
  • Modular, portable configurations

Expanded Matchers (v2.1.0)

v2.1.0 introduced specific matcher values for SessionStart and PreCompact events, allowing more granular control over when hooks execute.

SessionStart Matchers

Matcher Trigger
startup Session first starts
resume Session resumed from saved state
clear Session cleared by user
compact Session compacted (context condensed)

Example with SessionStart Matchers

{
  "SessionStart": [
    {
      "matcher": "startup",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/first-run-setup.sh",
          "once": true
        }
      ]
    },
    {
      "matcher": "resume",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/restore-state.sh"
        }
      ]
    }
  ]
}

PreCompact Matchers

Matcher Trigger
manual User-triggered compaction (/compact)
auto Automatic compaction (context limit)

Example with PreCompact Matchers

{
  "PreCompact": [
    {
      "matcher": "auto",
      "hooks": [
        {
          "type": "command",
          "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/backup-context.sh"
        }
      ]
    }
  ]
}

Matcher Behavior

  • Matchers are case-sensitive
  • Use * for wildcard (matches all)
  • Regex patterns supported for tool names

Middleware Pattern: updatedInput + permissionDecision (v2.1.0 Fix)

v2.1.0 fixed an issue where updatedInput and permissionDecision: 'ask' couldn't work together. PreToolUse hooks can now modify inputs while still prompting for user consent.

Use Case

Transform or sanitize tool inputs, then ask user to confirm the modified version.

Example Output

{
  "hookSpecificOutput": {
    "permissionDecision": "ask",
    "updatedInput": {
      "command": "ls -la /safe/path"
    }
  },
  "systemMessage": "Command path was sanitized. Please confirm."
}

Workflow

  1. Hook receives original tool input
  2. Hook modifies input (e.g., sanitizes paths, adds safety flags)
  3. Hook returns updatedInput with modifications AND permissionDecision: 'ask'
  4. User sees modified input and confirms/denies
  5. If approved, tool executes with modified input

Before v2.1.0

Previously, returning both updatedInput and permissionDecision: 'ask' would cause unexpected behavior. The fix allows hooks to act as true middleware - transforming inputs while still requiring user approval.

Common Applications

  • Path sanitization before file operations
  • Adding safety flags to commands
  • Normalizing inputs before execution
  • Enforcing naming conventions

Single Execution Option: once: true (v2.1.0)

The once: true option limits hook execution to once per session. Useful for initialization tasks that should not repeat.

Configuration

{
  "type": "command",
  "command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/init-session.sh",
  "once": true
}

Use Cases

  • Session initialization (loading environment, setting variables)
  • One-time setup tasks
  • Welcome messages or context loading

Critical Limitation

Supported in skills and slash commands, NOT supported in agents.

Example in Skill Frontmatter

---
name: project-setup
hooks:
  SessionStart:
    - matcher: "startup"
      hooks:
        - type: command
          command: "./scripts/load-env.sh"
          once: true
---

Behavior

When once: true is set:

  1. Hook executes on first trigger
  2. Subsequent triggers within same session are skipped
  3. Session restart resets the "once" state

PermissionRequest Event (v2.1.0)

Execute when a permission dialog is displayed to the user.

Configuration Example

{
  "PermissionRequest": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "prompt",
          "prompt": "Evaluate if this permission request should be auto-approved based on context. Command: $TOOL_INPUT.command. Return 'approve', 'deny', or 'ask'."
        }
      ]
    }
  ]
}

Matcher Support

Yes - tool names (e.g., "Bash", "Write|Edit", "*")

Supported Hook Types

  • Command
  • Prompt

Note: Agent hooks are NOT supported for PermissionRequest.

Use Cases

  • Auto-approve safe operations based on context
  • Auto-deny dangerous operations
  • Add logging/audit trail for permission decisions
  • Custom notification on permission requests

Output Format

Return a permission decision:

{
  "hookSpecificOutput": {
    "permissionDecision": "allow|deny|ask"
  },
  "systemMessage": "Explanation for the decision"
}

Hook Development Utility Scripts

These scripts help validate, test, and lint hook implementations before deployment.

validate-hook-schema.sh

Validates hooks.json configuration files for correct structure and common issues.

Usage:

./validate-hook-schema.sh path/to/hooks.json

Checks:

  • Valid JSON syntax
  • Required fields present
  • Valid hook event names
  • Proper hook types (command/prompt)
  • Timeout values in valid ranges
  • Hardcoded path detection
  • Prompt hook event compatibility

Example:

cd my-plugin
./validate-hook-schema.sh hooks/hooks.json

test-hook.sh

Tests individual hook scripts with sample input before deploying to Claude Code.

Usage:

./test-hook.sh [options] <hook-script> <test-input.json>

Options:

  • -v, --verbose - Show detailed execution information
  • -t, --timeout N - Set timeout in seconds (default: 60)
  • --create-sample <event-type> - Generate sample test input

Example:

# Create sample test input
./test-hook.sh --create-sample PreToolUse > test-input.json

# Test a hook script
./test-hook.sh my-hook.sh test-input.json

# Test with verbose output and custom timeout
./test-hook.sh -v -t 30 my-hook.sh test-input.json

Features:

  • Sets up proper environment variables (CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT)
  • Measures execution time
  • Validates output JSON
  • Shows exit codes and their meanings
  • Captures environment file output

hook-linter.sh

Checks hook scripts for common issues and best practices violations.

Usage:

./hook-linter.sh <hook-script.sh> [hook-script2.sh ...]

Checks:

  • Shebang presence
  • set -euo pipefail usage
  • Stdin input reading
  • Proper error handling
  • Variable quoting (injection prevention)
  • Exit code usage
  • Hardcoded paths
  • Long-running code detection
  • Error output to stderr
  • Input validation

Example:

# Lint single script
./hook-linter.sh ../examples/validate-write.sh

# Lint multiple scripts
./hook-linter.sh ../examples/*.sh

Typical Workflow

  1. Write your hook script

    vim my-plugin/scripts/my-hook.sh
  2. Lint the script

    ./hook-linter.sh my-plugin/scripts/my-hook.sh
  3. Create test input

    ./test-hook.sh --create-sample PreToolUse > test-input.json
    # Edit test-input.json as needed
  4. Test the hook

    ./test-hook.sh -v my-plugin/scripts/my-hook.sh test-input.json
  5. Add to hooks.json

    # Edit my-plugin/hooks/hooks.json
  6. Validate configuration

    ./validate-hook-schema.sh my-plugin/hooks/hooks.json
  7. Test in Claude Code

    claude --debug

Tips

  • Always test hooks before deploying to avoid breaking user workflows
  • Use verbose mode (-v) to debug hook behavior
  • Check the linter output for security and best practice issues
  • Validate hooks.json after any changes
  • Create different test inputs for various scenarios (safe operations, dangerous operations, edge cases)

Common Issues

Hook doesn't execute

Check:

  • Script has shebang (#!/bin/bash)
  • Script is executable (chmod +x)
  • Path in hooks.json is correct (use ${CLAUDE_PLUGIN_ROOT})

Hook times out

  • Reduce timeout in hooks.json
  • Optimize hook script performance
  • Remove long-running operations

Hook fails silently

  • Check exit codes (should be 0 or 2)
  • Ensure errors go to stderr (>&2)
  • Validate JSON output structure

Injection vulnerabilities

  • Always quote variables: "$variable"
  • Use set -euo pipefail
  • Validate all input fields
  • Run the linter to catch issues

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", "implement agent hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", "frontmatter hooks", "scoped hooks", "once: true", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification, PermissionRequest). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API and v2.1.0 features including agent hooks, frontmatter integration, and the once option. version: 0.2.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

v2.1.0: Agent hooks now available. See references/v2.1.0/agent-hooks.md

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

v2.1.0: PermissionRequest event added. See references/v2.1.0/permission-request.md

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.

v2.1.0: New matchers (startup, resume, clear, compact). See references/v2.1.0/matchers.md

SessionEnd

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

PreCompact

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

v2.1.0: New matchers (manual, auto). See references/v2.1.0/matchers.md

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 ${CLAUDE_PLUGIN_ROOT} 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 ${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
  • references/v2.1.0/ - Claude Code v2.1.0 hook features (agent hooks, frontmatter, matchers)

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 ${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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment