Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 39 additions & 35 deletions plugins/plugin-dev/skills/hook-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,47 +339,51 @@ Available in all command hooks:

## Plugin Hook Configuration

In plugins, define hooks in `hooks/hooks.json`:
In plugins, define hooks in `hooks/hooks.json` using the **wrapper format**:

```json
{
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "Validate file write safety"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Verify task completion"
}
]
}
],
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh",
"timeout": 10
}
]
}
]
"description": "Plugin hooks for validation and context loading",
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "Validate file write safety"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "prompt",
"prompt": "Verify task completion"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/scripts/load-context.sh",
"timeout": 10
}
]
}
]
}
}
```

**Important:** Plugin hooks.json must use the wrapper format (`{"hooks": {...}}`), not the direct format used in settings files. The `matcher` field is optional for SessionStart, SessionEnd, PreCompact, and Notification events.

Plugin hooks merge with user's hooks and run in parallel.

## Matchers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,24 @@ if ! jq empty "$HOOKS_FILE" 2>/dev/null; then
fi
echo "✅ Valid JSON"

# Detect format: plugin wrapper format {"hooks": {...}} vs direct format
# Plugin hooks.json uses: {"description": "...", "hooks": {"SessionStart": [...]}}
# Settings format uses: {"SessionStart": [...]}
JQ_PREFIX=""
if jq -e '.hooks' "$HOOKS_FILE" >/dev/null 2>&1 && jq -e '.hooks | type == "object"' "$HOOKS_FILE" >/dev/null 2>&1; then
echo "Detected plugin wrapper format"
JQ_PREFIX=".hooks"
else
echo "Detected direct format"
JQ_PREFIX="."
fi

# 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
for event in $(jq -r "$JQ_PREFIX | keys[]" "$HOOKS_FILE"); do
found=false
for valid_event in "${VALID_EVENTS[@]}"; do
if [ "$event" = "$valid_event" ]; then
Expand All @@ -62,31 +74,37 @@ 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 "$JQ_PREFIX | keys[]" "$HOOKS_FILE"); do
hook_count=$(jq -r "$JQ_PREFIX.\"$event\" | length" "$HOOKS_FILE")

for ((i=0; i<hook_count; i++)); do
# Check matcher exists
matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
# Check matcher (optional for all events; defaults to matching everything)
# For tool-specific events, warn if missing since it likely should be explicit
matcher=$(jq -r "$JQ_PREFIX.\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
if [ -z "$matcher" ]; then
echo "❌ $event[$i]: Missing 'matcher' field"
((error_count++))
continue
is_tool_event=false
if [ "$event" = "PreToolUse" ] || [ "$event" = "PostToolUse" ]; then
is_tool_event=true
fi
if [ "$is_tool_event" = true ]; then
echo "⚠️ $event[$i]: No 'matcher' field — hook will match all tools"
((warning_count++))
fi
fi

# Check hooks array exists
hooks=$(jq -r ".\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
hooks=$(jq -r "$JQ_PREFIX.\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then
echo "❌ $event[$i]: Missing 'hooks' array"
((error_count++))
continue
fi

# Validate each hook in the array
hook_array_count=$(jq -r ".\"$event\"[$i].hooks | length" "$HOOKS_FILE")
hook_array_count=$(jq -r "$JQ_PREFIX.\"$event\"[$i].hooks | length" "$HOOKS_FILE")

for ((j=0; j<hook_array_count; j++)); do
hook_type=$(jq -r ".\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
hook_type=$(jq -r "$JQ_PREFIX.\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")

if [ -z "$hook_type" ]; then
echo "❌ $event[$i].hooks[$j]: Missing 'type' field"
Expand All @@ -102,7 +120,7 @@ for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do

# Check type-specific fields
if [ "$hook_type" = "command" ]; then
command=$(jq -r ".\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
command=$(jq -r "$JQ_PREFIX.\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
if [ -z "$command" ]; then
echo "❌ $event[$i].hooks[$j]: Command hooks must have 'command' field"
((error_count++))
Expand All @@ -114,7 +132,7 @@ for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
fi
fi
elif [ "$hook_type" = "prompt" ]; then
prompt=$(jq -r ".\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
prompt=$(jq -r "$JQ_PREFIX.\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
if [ -z "$prompt" ]; then
echo "❌ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field"
((error_count++))
Expand All @@ -128,7 +146,7 @@ for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
fi

# Check timeout
timeout=$(jq -r ".\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
timeout=$(jq -r "$JQ_PREFIX.\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
echo "❌ $event[$i].hooks[$j]: Timeout must be a number"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,16 @@ hooks/
└── load-context.sh
```

**hooks.json**:
**hooks.json** (plugin wrapper format):
```json
{
"PreToolUse": [...],
"PostToolUse": [...],
"Stop": [...],
"SessionStart": [...]
"description": "Plugin hooks",
"hooks": {
"PreToolUse": [...],
"PostToolUse": [...],
"Stop": [...],
"SessionStart": [...]
}
}
```

Expand Down