[BUG] validate-hook-schema.sh fails on plugin hook manifests and non-tool hooks (Fix included)

Status Open
Maintainer reply None cached
Activity 1 comment · opened Aug 28, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The validate-hook-schema.sh utility in plugins/plugin-dev fails when validating all official plugin hooks.json files in the repository, outputting dozens of false error messages.

Root causes:

  1. The script inspects top-level keys directly, failing on plugin hook files that use the standard {"description": "...", "hooks": { ... }} wrapper format documented in SKILL.md. It treats "description" and "hooks" as event names and iterates over characters in the description string as hook items.
  2. The script enforces "matcher" as a mandatory field for all hook events, even though non-tool events (Stop, SessionStart, UserPromptSubmit, etc.) do not use or require a matcher.

What Should Happen?

The validator should support both the plugin wrapper format {"hooks": { ... }} (with optional "description") and the settings format { "<Event>": [ ... ] }, and should only require matcher for tool events (PreToolUse and PostToolUse).

Error Messages/Logs

Steps to Reproduce

  1. Run ./plugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh plugins/explanatory-output-style/hooks/hooks.json (or on any plugin hooks.json in the repository).
  2. Observe validation failure with 38+ false errors.

Claude Model

None

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

Latest (main)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Terminal.app (macOS)

Additional Information

I have prepared and verified a minimal fix on my fork:

Proposed Diff:

--- a/plugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh
+++ b/plugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh
@@ -35,12 +35,24 @@ if ! jq empty "$HOOKS_FILE" 2>/dev/null; then
 fi
 echo "✅ Valid JSON"
 
+# Determine hook root expression (support both plugin wrapper format {"hooks": {...}} and settings direct format)
+HOOK_ROOT_JQ='if (type == "object" and has("hooks") and (.hooks | type == "object")) then .hooks else . end'
+HAS_HOOKS_WRAPPER=$(jq -r 'if (type == "object" and has("hooks") and (.hooks | type == "object")) then "true" else "false" end' "$HOOKS_FILE")
+
 # 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
+if [ "$HAS_HOOKS_WRAPPER" = "true" ]; then
+  for key in $(jq -r 'keys[]' "$HOOKS_FILE" | tr -d '\r'); do
+    if [ "$key" != "hooks" ] && [ "$key" != "description" ]; then
+      echo "⚠️  Unknown top-level field in plugin format: $key"
+    fi
+  done
+fi
+
+for event in $(jq -r "($HOOK_ROOT_JQ) | keys[]" "$HOOKS_FILE" | tr -d '\r'); do
   found=false
   for valid_event in "${VALID_EVENTS[@]}"; do
     if [ "$event" = "$valid_event" ]; then
@@ -62,20 +74,22 @@ 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 event in $(jq -r "($HOOK_ROOT_JQ) | keys[]" "$HOOKS_FILE" | tr -d '\r'); do
+  hook_count=$(jq -r "($HOOK_ROOT_JQ) | .[\"$event\"] | length" "$HOOKS_FILE" | tr -d '\r')
 
   for ((i=0; i<hook_count; i++)); do
-    # Check matcher exists
-    matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
+    # Check matcher (only required for tool-level events: PreToolUse, PostToolUse)
+    matcher=$(jq -r "($HOOK_ROOT_JQ) | .[\"$event\"][$i].matcher // empty" "$HOOKS_FILE")
     if [ -z "$matcher" ]; then
-      echo "❌ $event[$i]: Missing 'matcher' field"
-      ((error_count++))
-      continue
+      if [ "$event" = "PreToolUse" ] || [ "$event" = "PostToolUse" ]; then
+        echo "❌ $event[$i]: Missing 'matcher' field"
+        ((error_count++))
+        continue
+      fi
     fi

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗