spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / plugins / plugin-dev / skills / hook-development / scripts / validate-hook-schema.sh
159 lines · 5.0 KBRaw
1#!/bin/bash
2# Hook Schema Validator
3# Validates hooks.json structure and checks for common issues
4
5set -euo pipefail
6
7# Usage
8if [ $# -eq 0 ]; then
9 echo "Usage: $0 <path/to/hooks.json>"
10 echo ""
11 echo "Validates hook configuration file for:"
12 echo " - Valid JSON syntax"
13 echo " - Required fields"
14 echo " - Hook type validity"
15 echo " - Matcher patterns"
16 echo " - Timeout ranges"
17 exit 1
18fi
19
20HOOKS_FILE="$1"
21
22if [ ! -f "$HOOKS_FILE" ]; then
23 echo "❌ Error: File not found: $HOOKS_FILE"
24 exit 1
25fi
26
27echo "🔍 Validating hooks configuration: $HOOKS_FILE"
28echo ""
29
30# Check 1: Valid JSON
31echo "Checking JSON syntax..."
32if ! jq empty "$HOOKS_FILE" 2>/dev/null; then
33 echo "❌ Invalid JSON syntax"
34 exit 1
35fi
36echo "✅ Valid JSON"
37
38# Check 2: Root structure
39echo ""
40echo "Checking root structure..."
41VALID_EVENTS=("PreToolUse" "PostToolUse" "UserPromptSubmit" "Stop" "SubagentStop" "SessionStart" "SessionEnd" "PreCompact" "Notification")
42
43for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
44 found=false
45 for valid_event in "${VALID_EVENTS[@]}"; do
46 if [ "$event" = "$valid_event" ]; then
47 found=true
48 break
49 fi
50 done
51
52 if [ "$found" = false ]; then
53 echo "⚠️ Unknown event type: $event"
54 fi
55done
56echo "✅ Root structure valid"
57
58# Check 3: Validate each hook
59echo ""
60echo "Validating individual hooks..."
61
62error_count=0
63warning_count=0
64
65for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
66 hook_count=$(jq -r ".\"$event\" | length" "$HOOKS_FILE")
67
68 for ((i=0; i<hook_count; i++)); do
69 # Check matcher exists
70 matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
71 if [ -z "$matcher" ]; then
72 echo "❌ $event[$i]: Missing 'matcher' field"
73 ((error_count++))
74 continue
75 fi
76
77 # Check hooks array exists
78 hooks=$(jq -r ".\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
79 if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then
80 echo "❌ $event[$i]: Missing 'hooks' array"
81 ((error_count++))
82 continue
83 fi
84
85 # Validate each hook in the array
86 hook_array_count=$(jq -r ".\"$event\"[$i].hooks | length" "$HOOKS_FILE")
87
88 for ((j=0; j<hook_array_count; j++)); do
89 hook_type=$(jq -r ".\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
90
91 if [ -z "$hook_type" ]; then
92 echo "❌ $event[$i].hooks[$j]: Missing 'type' field"
93 ((error_count++))
94 continue
95 fi
96
97 if [ "$hook_type" != "command" ] && [ "$hook_type" != "prompt" ]; then
98 echo "❌ $event[$i].hooks[$j]: Invalid type '$hook_type' (must be 'command' or 'prompt')"
99 ((error_count++))
100 continue
101 fi
102
103 # Check type-specific fields
104 if [ "$hook_type" = "command" ]; then
105 command=$(jq -r ".\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
106 if [ -z "$command" ]; then
107 echo "❌ $event[$i].hooks[$j]: Command hooks must have 'command' field"
108 ((error_count++))
109 else
110 # Check for hardcoded paths
111 if [[ "$command" == /* ]] && [[ "$command" != *'${CLAUDE_PLUGIN_ROOT}'* ]]; then
112 echo "⚠️ $event[$i].hooks[$j]: Hardcoded absolute path detected. Consider using \${CLAUDE_PLUGIN_ROOT}"
113 ((warning_count++))
114 fi
115 fi
116 elif [ "$hook_type" = "prompt" ]; then
117 prompt=$(jq -r ".\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
118 if [ -z "$prompt" ]; then
119 echo "❌ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field"
120 ((error_count++))
121 fi
122
123 # Check if prompt-based hooks are used on supported events
124 if [ "$event" != "Stop" ] && [ "$event" != "SubagentStop" ] && [ "$event" != "UserPromptSubmit" ] && [ "$event" != "PreToolUse" ]; then
125 echo "⚠️ $event[$i].hooks[$j]: Prompt hooks may not be fully supported on $event (best on Stop, SubagentStop, UserPromptSubmit, PreToolUse)"
126 ((warning_count++))
127 fi
128 fi
129
130 # Check timeout
131 timeout=$(jq -r ".\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
132 if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then
133 if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
134 echo "❌ $event[$i].hooks[$j]: Timeout must be a number"
135 ((error_count++))
136 elif [ "$timeout" -gt 600 ]; then
137 echo "⚠️ $event[$i].hooks[$j]: Timeout $timeout seconds is very high (max 600s)"
138 ((warning_count++))
139 elif [ "$timeout" -lt 5 ]; then
140 echo "⚠️ $event[$i].hooks[$j]: Timeout $timeout seconds is very low"
141 ((warning_count++))
142 fi
143 fi
144 done
145 done
146done
147
148echo ""
149echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
150if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
151 echo "✅ All checks passed!"
152 exit 0
153elif [ $error_count -eq 0 ]; then
154 echo "⚠️ Validation passed with $warning_count warning(s)"
155 exit 0
156else
157 echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)"
158 exit 1
159fi