Initial commit
This commit is contained in:
711
skills/hook-development/SKILL.md
Normal file
711
skills/hook-development/SKILL.md
Normal file
@@ -0,0 +1,711 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
# 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.
|
||||
55
skills/hook-development/examples/load-context.sh
Executable file
55
skills/hook-development/examples/load-context.sh
Executable 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
|
||||
43
skills/hook-development/examples/validate-bash.sh
Executable file
43
skills/hook-development/examples/validate-bash.sh
Executable 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
|
||||
38
skills/hook-development/examples/validate-write.sh
Executable file
38
skills/hook-development/examples/validate-write.sh
Executable 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
|
||||
479
skills/hook-development/references/advanced.md
Normal file
479
skills/hook-development/references/advanced.md
Normal 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.
|
||||
369
skills/hook-development/references/migration.md
Normal file
369
skills/hook-development/references/migration.md
Normal 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.
|
||||
346
skills/hook-development/references/patterns.md
Normal file
346
skills/hook-development/references/patterns.md
Normal 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
|
||||
164
skills/hook-development/scripts/README.md
Normal file
164
skills/hook-development/scripts/README.md
Normal 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
|
||||
153
skills/hook-development/scripts/hook-linter.sh
Executable file
153
skills/hook-development/scripts/hook-linter.sh
Executable 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
|
||||
252
skills/hook-development/scripts/test-hook.sh
Executable file
252
skills/hook-development/scripts/test-hook.sh
Executable 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
|
||||
159
skills/hook-development/scripts/validate-hook-schema.sh
Executable file
159
skills/hook-development/scripts/validate-hook-schema.sh
Executable 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
|
||||
Reference in New Issue
Block a user