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
| 1 | #!/bin/bash |
| 2 | # Hook Schema Validator |
| 3 | # Validates hooks.json structure and checks for common issues |
| 4 | |
| 5 | set -euo pipefail |
| 6 | |
| 7 | # Usage |
| 8 | if [ $# -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 |
| 18 | fi |
| 19 | |
| 20 | HOOKS_FILE="$1" |
| 21 | |
| 22 | if [ ! -f "$HOOKS_FILE" ]; then |
| 23 | echo "❌ Error: File not found: $HOOKS_FILE" |
| 24 | exit 1 |
| 25 | fi |
| 26 | |
| 27 | echo "🔍 Validating hooks configuration: $HOOKS_FILE" |
| 28 | echo "" |
| 29 | |
| 30 | # Check 1: Valid JSON |
| 31 | echo "Checking JSON syntax..." |
| 32 | if ! jq empty "$HOOKS_FILE" 2>/dev/null; then |
| 33 | echo "❌ Invalid JSON syntax" |
| 34 | exit 1 |
| 35 | fi |
| 36 | echo "✅ Valid JSON" |
| 37 | |
| 38 | # Check 2: Root structure |
| 39 | echo "" |
| 40 | echo "Checking root structure..." |
| 41 | VALID_EVENTS=("PreToolUse" "PostToolUse" "UserPromptSubmit" "Stop" "SubagentStop" "SessionStart" "SessionEnd" "PreCompact" "Notification") |
| 42 | |
| 43 | for 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 |
| 55 | done |
| 56 | echo "✅ Root structure valid" |
| 57 | |
| 58 | # Check 3: Validate each hook |
| 59 | echo "" |
| 60 | echo "Validating individual hooks..." |
| 61 | |
| 62 | error_count=0 |
| 63 | warning_count=0 |
| 64 | |
| 65 | for 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 |
| 146 | done |
| 147 | |
| 148 | echo "" |
| 149 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 150 | if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then |
| 151 | echo "✅ All checks passed!" |
| 152 | exit 0 |
| 153 | elif [ $error_count -eq 0 ]; then |
| 154 | echo "⚠️ Validation passed with $warning_count warning(s)" |
| 155 | exit 0 |
| 156 | else |
| 157 | echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)" |
| 158 | exit 1 |
| 159 | fi |