Initial commit
This commit is contained in:
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