Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:55:59 +08:00
commit 7449ea6e8b
60 changed files with 21848 additions and 0 deletions

View File

@@ -0,0 +1,712 @@
---
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.

View File

@@ -0,0 +1,55 @@
#!/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

View File

@@ -0,0 +1,43 @@
#!/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

View File

@@ -0,0 +1,38 @@
#!/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

View File

@@ -0,0 +1,479 @@
# 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.

View File

@@ -0,0 +1,369 @@
# 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.

View File

@@ -0,0 +1,346 @@
# 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

View File

@@ -0,0 +1,164 @@
# 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:**
```bash
./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:**
```bash
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:**
```bash
./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:**
```bash
# 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:**
```bash
./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:**
```bash
# 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**
```bash
vim my-plugin/scripts/my-hook.sh
```
2. **Lint the script**
```bash
./hook-linter.sh my-plugin/scripts/my-hook.sh
```
3. **Create test input**
```bash
./test-hook.sh --create-sample PreToolUse > test-input.json
# Edit test-input.json as needed
```
4. **Test the hook**
```bash
./test-hook.sh -v my-plugin/scripts/my-hook.sh test-input.json
```
5. **Add to hooks.json**
```bash
# Edit my-plugin/hooks/hooks.json
```
6. **Validate configuration**
```bash
./validate-hook-schema.sh my-plugin/hooks/hooks.json
```
7. **Test in Claude Code**
```bash
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

View File

@@ -0,0 +1,153 @@
#!/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

View File

@@ -0,0 +1,252 @@
#!/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

View File

@@ -0,0 +1,159 @@
#!/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