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

fix: Add missing matcher fields to hooks.json filesBrowse files
Stop hooks (and other hook types) were not firing because they were
missing the required "matcher" field. According to the hook development
documentation, all hooks must have a matcher field - "*" for wildcard
matching.

Changes:
- Add matcher: "*" to all hooks in hookify, ralph-wiggum,
  explanatory-output-style, and learning-output-style plugins
- Update validate-hook-schema.sh to properly handle plugin format
  (with 'hooks' wrapper) vs settings format (events at root)
- Add validate-all-hooks.sh script to validate all hooks.json files

Fixes: https://anthropic.slack.com/archives/C08EHE6JF3L/p1765822035850959
Claude authored on Dec 15, 2025 parent eb87245 · commit 411381bf3e91
6 changed files+86 -10compared with eb87245
Mplugins/explanatory-output-style/hooks/hooks.json
Mplugins/hookify/hooks/hooks.json
Mplugins/learning-output-style/hooks/hooks.json
Mplugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh
Mplugins/ralph-wiggum/hooks/hooks.json
Ascripts/validate-all-hooks.sh
@@ -3,6 +3,7 @@
33 "hooks": {
44 "SessionStart": [
55 {
6+ "matcher": "*",
67 "hooks": [
78 {
89 "type": "command",
@@ -3,6 +3,7 @@
33 "hooks": {
44 "PreToolUse": [
55 {
6+ "matcher": "*",
67 "hooks": [
78 {
89 "type": "command",
@@ -14,6 +15,7 @@
1415 ],
1516 "PostToolUse": [
1617 {
18+ "matcher": "*",
1719 "hooks": [
1820 {
1921 "type": "command",
@@ -25,6 +27,7 @@
2527 ],
2628 "Stop": [
2729 {
30+ "matcher": "*",
2831 "hooks": [
2932 {
3033 "type": "command",
@@ -36,6 +39,7 @@
3639 ],
3740 "UserPromptSubmit": [
3841 {
42+ "matcher": "*",
3943 "hooks": [
4044 {
4145 "type": "command",
@@ -3,6 +3,7 @@
33 "hooks": {
44 "SessionStart": [
55 {
6+ "matcher": "*",
67 "hooks": [
78 {
89 "type": "command",
@@ -40,7 +40,27 @@
4040 echo "Checking root structure..."
4141 VALID_EVENTS=("PreToolUse" "PostToolUse" "UserPromptSubmit" "Stop" "SubagentStop" "SessionStart" "SessionEnd" "PreCompact" "Notification")
4242
43-for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
43+# Detect format: plugin format has { description?, hooks: {...} } wrapper
44+# Settings format has events directly at root level
45+is_plugin_format=false
46+if 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
57+else
58+ HOOKS_PATH="."
59+ echo "Detected settings format (events at root)"
60+fi
61+
62+# Validate event types
63+for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
4464 found=false
4565 for valid_event in "${VALID_EVENTS[@]}"; do
4666 if [ "$event" = "$valid_event" ]; then
@@ -62,12 +82,12 @@
6282 error_count=0
6383 warning_count=0
6484
65-for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
66- hook_count=$(jq -r ".\"$event\" | length" "$HOOKS_FILE")
85+for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
86+ hook_count=$(jq -r "$HOOKS_PATH.\"$event\" | length" "$HOOKS_FILE")
6787
6888 for ((i=0; i<hook_count; i++)); do
6989 # Check matcher exists
70- matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
90+ matcher=$(jq -r "$HOOKS_PATH.\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
7191 if [ -z "$matcher" ]; then
7292 echo "❌ $event[$i]: Missing 'matcher' field"
7393 ((error_count++))
@@ -75,7 +95,7 @@
7595 fi
7696
7797 # Check hooks array exists
78- hooks=$(jq -r ".\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
98+ hooks=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
7999 if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then
80100 echo "❌ $event[$i]: Missing 'hooks' array"
81101 ((error_count++))
@@ -83,10 +103,10 @@
83103 fi
84104
85105 # Validate each hook in the array
86- hook_array_count=$(jq -r ".\"$event\"[$i].hooks | length" "$HOOKS_FILE")
106+ hook_array_count=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks | length" "$HOOKS_FILE")
87107
88108 for ((j=0; j<hook_array_count; j++)); do
89- hook_type=$(jq -r ".\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
109+ hook_type=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
90110
91111 if [ -z "$hook_type" ]; then
92112 echo "❌ $event[$i].hooks[$j]: Missing 'type' field"
@@ -102,7 +122,7 @@
102122
103123 # Check type-specific fields
104124 if [ "$hook_type" = "command" ]; then
105- command=$(jq -r ".\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
125+ command=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
106126 if [ -z "$command" ]; then
107127 echo "❌ $event[$i].hooks[$j]: Command hooks must have 'command' field"
108128 ((error_count++))
@@ -114,7 +134,7 @@
114134 fi
115135 fi
116136 elif [ "$hook_type" = "prompt" ]; then
117- prompt=$(jq -r ".\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
137+ prompt=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
118138 if [ -z "$prompt" ]; then
119139 echo "❌ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field"
120140 ((error_count++))
@@ -128,7 +148,7 @@
128148 fi
129149
130150 # Check timeout
131- timeout=$(jq -r ".\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
151+ timeout=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
132152 if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then
133153 if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
134154 echo "❌ $event[$i].hooks[$j]: Timeout must be a number"
@@ -3,6 +3,7 @@
33 "hooks": {
44 "Stop": [
55 {
6+ "matcher": "*",
67 "hooks": [
78 {
89 "type": "command",
@@ -1,0 +1,49 @@
1+#!/bin/bash
2+# Validate all hooks.json files in the repository
3+# This script can be run in CI to ensure all plugins have valid hook configurations
4+
5+set -euo pipefail
6+
7+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+REPO_ROOT="$(dirname "$SCRIPT_DIR")"
9+VALIDATOR="$REPO_ROOT/plugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh"
10+
11+echo "🔍 Validating all hooks.json files in the repository..."
12+echo ""
13+
14+# Find all hooks.json files
15+mapfile -t HOOKS_FILES < <(find "$REPO_ROOT/plugins" -name "hooks.json" -type f 2>/dev/null)
16+
17+if [ ${#HOOKS_FILES[@]} -eq 0 ]; then
18+ echo "No hooks.json files found"
19+ exit 0
20+fi
21+
22+echo "Found ${#HOOKS_FILES[@]} hooks.json file(s)"
23+echo ""
24+
25+errors=0
26+for hooks_file in "${HOOKS_FILES[@]}"; do
27+ relative_path="${hooks_file#$REPO_ROOT/}"
28+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
29+ echo "📄 $relative_path"
30+ echo ""
31+
32+ if bash "$VALIDATOR" "$hooks_file"; then
33+ echo ""
34+ else
35+ echo ""
36+ ((errors++))
37+ fi
38+done
39+
40+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
41+echo ""
42+
43+if [ $errors -eq 0 ]; then
44+ echo "✅ All ${#HOOKS_FILES[@]} hooks.json file(s) are valid!"
45+ exit 0
46+else
47+ echo "❌ $errors hooks.json file(s) have validation errors"
48+ exit 1
49+fi