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
179 lines · 5.7 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
43# Detect format: plugin format has { description?, hooks: {...} } wrapper
44# Settings format has events directly at root level
45is_plugin_format=false
46if jq -e '.hooks' "$HOOKS_FILE" >/dev/null 2>&1; then
47 is_plugin_format=true
48 HOOKS_PATH=".hooks"
49 echo "Detected plugin format (with 'hooks' wrapper)"
50
51 # Validate allowed root keys for plugin format
52 for key in $(jq -r 'keys[]' "$HOOKS_FILE"); do
53 if [ "$key" != "hooks" ] && [ "$key" != "description" ]; then
54 echo "⚠️ Unknown root key in plugin format: $key (expected: 'hooks', 'description')"
55 fi
56 done
57else
58 HOOKS_PATH="."
59 echo "Detected settings format (events at root)"
60fi
61
62# Validate event types
63for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
64 found=false
65 for valid_event in "${VALID_EVENTS[@]}"; do
66 if [ "$event" = "$valid_event" ]; then
67 found=true
68 break
69 fi
70 done
71
72 if [ "$found" = false ]; then
73 echo "⚠️ Unknown event type: $event"
74 fi
75done
76echo "✅ Root structure valid"
77
78# Check 3: Validate each hook
79echo ""
80echo "Validating individual hooks..."
81
82error_count=0
83warning_count=0
84
85for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
86 hook_count=$(jq -r "$HOOKS_PATH.\"$event\" | length" "$HOOKS_FILE")
87
88 for ((i=0; i<hook_count; i++)); do
89 # Check matcher exists
90 matcher=$(jq -r "$HOOKS_PATH.\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
91 if [ -z "$matcher" ]; then
92 echo "❌ $event[$i]: Missing 'matcher' field"
93 ((error_count++))
94 continue
95 fi
96
97 # Check hooks array exists
98 hooks=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
99 if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then
100 echo "❌ $event[$i]: Missing 'hooks' array"
101 ((error_count++))
102 continue
103 fi
104
105 # Validate each hook in the array
106 hook_array_count=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks | length" "$HOOKS_FILE")
107
108 for ((j=0; j<hook_array_count; j++)); do
109 hook_type=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
110
111 if [ -z "$hook_type" ]; then
112 echo "❌ $event[$i].hooks[$j]: Missing 'type' field"
113 ((error_count++))
114 continue
115 fi
116
117 if [ "$hook_type" != "command" ] && [ "$hook_type" != "prompt" ]; then
118 echo "❌ $event[$i].hooks[$j]: Invalid type '$hook_type' (must be 'command' or 'prompt')"
119 ((error_count++))
120 continue
121 fi
122
123 # Check type-specific fields
124 if [ "$hook_type" = "command" ]; then
125 command=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
126 if [ -z "$command" ]; then
127 echo "❌ $event[$i].hooks[$j]: Command hooks must have 'command' field"
128 ((error_count++))
129 else
130 # Check for hardcoded paths
131 if [[ "$command" == /* ]] && [[ "$command" != *'${CLAUDE_PLUGIN_ROOT}'* ]]; then
132 echo "⚠️ $event[$i].hooks[$j]: Hardcoded absolute path detected. Consider using \${CLAUDE_PLUGIN_ROOT}"
133 ((warning_count++))
134 fi
135 fi
136 elif [ "$hook_type" = "prompt" ]; then
137 prompt=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
138 if [ -z "$prompt" ]; then
139 echo "❌ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field"
140 ((error_count++))
141 fi
142
143 # Check if prompt-based hooks are used on supported events
144 if [ "$event" != "Stop" ] && [ "$event" != "SubagentStop" ] && [ "$event" != "UserPromptSubmit" ] && [ "$event" != "PreToolUse" ]; then
145 echo "⚠️ $event[$i].hooks[$j]: Prompt hooks may not be fully supported on $event (best on Stop, SubagentStop, UserPromptSubmit, PreToolUse)"
146 ((warning_count++))
147 fi
148 fi
149
150 # Check timeout
151 timeout=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
152 if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then
153 if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
154 echo "❌ $event[$i].hooks[$j]: Timeout must be a number"
155 ((error_count++))
156 elif [ "$timeout" -gt 600 ]; then
157 echo "⚠️ $event[$i].hooks[$j]: Timeout $timeout seconds is very high (max 600s)"
158 ((warning_count++))
159 elif [ "$timeout" -lt 5 ]; then
160 echo "⚠️ $event[$i].hooks[$j]: Timeout $timeout seconds is very low"
161 ((warning_count++))
162 fi
163 fi
164 done
165 done
166done
167
168echo ""
169echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
170if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
171 echo "✅ All checks passed!"
172 exit 0
173elif [ $error_count -eq 0 ]; then
174 echo "⚠️ Validation passed with $warning_count warning(s)"
175 exit 0
176else
177 echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)"
178 exit 1
179fi