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
Install
npx skills add https://github.com/tzachbon/smart-ralph/tree/main/.agents/skills/Hook Development
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tzachbon-smart-ralph@llmmart
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:
descriptionfield is optionalhooksfield 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
- Use command hooks for quick deterministic checks
- Use prompt hooks for complex reasoning
- Cache validation results in temp files
- 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.jsonwon'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
claudeagain
To test hook changes:
- Edit hook configuration or scripts
- Exit Claude Code session
- Restart:
claudeorcc - New hook configuration loads
- 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 hooksreferences/advanced.md- Advanced use cases and techniques
Example Hook Scripts
Working examples in examples/:
validate-write.sh- File write validation examplevalidate-bash.sh- Bash command validation exampleload-context.sh- SessionStart context loading example
Utility Scripts
Development tools in scripts/:
validate-hook-schema.sh- Validate hooks.json structure and syntaxtest-hook.sh- Test hooks with sample input before deploymenthook-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 --debugfor detailed logs - Validation: Use
jqto validate hook JSON output
Implementation Workflow
To implement hooks in a plugin:
- Identify events to hook into (PreToolUse, Stop, SessionStart, etc.)
- Decide between prompt-based (flexible) or command (deterministic) hooks
- Write hook configuration in
hooks/hooks.json - For command hooks, create hook scripts
- Use $ for all file references
- Validate configuration with
scripts/validate-hook-schema.sh hooks/hooks.json - Test hooks with
scripts/test-hook.shbefore deployment - Test in Claude Code with
claude --debug - 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.
Reviews (0)
No reviews yet.
No comments yet.