validate-hook-schema.sh 5.0 KB

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