hooks.json uses flat structure instead of required nested hooks array
Affected repos: zetetic-team-subagents (v2.0.0), Cortex (v3.9.0)
Files: hooks/hooks.json (subagents), .claude/hooks/hooks.json (Cortex)
Severity: Plugin fails to load — all hooks are broken
Problem
Claude Code's hook schema requires each hook entry to wrap type/command inside a nested hooks array. Both repos use a flat structure that puts type and command as siblings of matcher/when, which causes validation to reject every single hook entry:
Failed to load hooks from hooks.json:
expected: "array", code: "invalid_type",
path: ["hooks", "PreToolUse", 0, "hooks"],
message: "Invalid input: expected array, received undefined"
This repeats for all 14 hook entries across PreToolUse (5), PostToolUse (4), PostToolUseFailure (1), SessionStart (2), Stop (1), and Notification (1).
Diff
The fix is mechanical — wrap each entry's type/command in a hooks: [...] array. The matcher and when fields stay at the outer level.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"when": "command contains 'git commit'",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-zetetic.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-zetetic.sh"
+ }
+ ]
},
{
"matcher": "Bash",
"when": "command contains 'git push'",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-review.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-review.sh"
+ }
+ ]
},
{
"matcher": "Bash",
"when": "command contains 'git push'",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-provenance.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-provenance.sh"
+ }
+ ]
},
{
"matcher": "Edit|Write",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-edit-layer-check.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-edit-layer-check.sh"
+ }
+ ]
},
{
"matcher": "Edit|Write",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-claim-gate.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-claim-gate.sh"
+ }
+ ]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"when": "command contains 'git commit'",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-difficulty.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-difficulty.sh"
+ }
+ ]
},
{
"matcher": "Bash",
"when": "command contains 'git commit'",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-lab-notebook.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-lab-notebook.sh"
+ }
+ ]
},
{
"matcher": "Edit|Write",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-balance.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-balance.sh"
+ }
+ ]
},
{
"matcher": "WebFetch|WebSearch",
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-research-provenance.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-research-provenance.sh"
+ }
+ ]
}
],
"PostToolUseFailure": [
{
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-error-routing.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-error-routing.sh"
+ }
+ ]
}
],
"SessionStart": [
{
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
+ }
+ ]
},
{
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start-research.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start-research.sh"
+ }
+ ]
}
],
"Stop": [
{
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh"
+ }
+ ]
}
],
"Notification": [
{
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/notification-handler.sh"
+ "hooks": [
+ {
+ "type": "command",
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/notification-handler.sh"
+ }
+ ]
}
]
}
}
Reference
Working example from token-optimizer (v5.1.0) that uses the correct nested schema:
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "python3 '${CLAUDE_PLUGIN_ROOT}/skills/token-optimizer/scripts/read_cache.py' --quiet"
}
]
}
hooks.json uses flat structure instead of required nested
hooksarrayAffected repos:
zetetic-team-subagents(v2.0.0),Cortex(v3.9.0)Files:
hooks/hooks.json(subagents),.claude/hooks/hooks.json(Cortex)Severity: Plugin fails to load — all hooks are broken
Problem
Claude Code's hook schema requires each hook entry to wrap
type/commandinside a nestedhooksarray. Both repos use a flat structure that putstypeandcommandas siblings ofmatcher/when, which causes validation to reject every single hook entry:This repeats for all 14 hook entries across PreToolUse (5), PostToolUse (4), PostToolUseFailure (1), SessionStart (2), Stop (1), and Notification (1).
Diff
The fix is mechanical — wrap each entry's
type/commandin ahooks: [...]array. Thematcherandwhenfields stay at the outer level.{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "when": "command contains 'git commit'", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-zetetic.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-zetetic.sh" + } + ] }, { "matcher": "Bash", "when": "command contains 'git push'", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-review.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-review.sh" + } + ] }, { "matcher": "Bash", "when": "command contains 'git push'", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-provenance.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-push-provenance.sh" + } + ] }, { "matcher": "Edit|Write", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-edit-layer-check.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-edit-layer-check.sh" + } + ] }, { "matcher": "Edit|Write", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-claim-gate.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-claim-gate.sh" + } + ] } ], "PostToolUse": [ { "matcher": "Bash", "when": "command contains 'git commit'", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-difficulty.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-difficulty.sh" + } + ] }, { "matcher": "Bash", "when": "command contains 'git commit'", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-lab-notebook.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-lab-notebook.sh" + } + ] }, { "matcher": "Edit|Write", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-balance.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-balance.sh" + } + ] }, { "matcher": "WebFetch|WebSearch", - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-research-provenance.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-research-provenance.sh" + } + ] } ], "PostToolUseFailure": [ { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-error-routing.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-error-routing.sh" + } + ] } ], "SessionStart": [ { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh" + } + ] }, { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start-research.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start-research.sh" + } + ] } ], "Stop": [ { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh" + } + ] } ], "Notification": [ { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/notification-handler.sh" + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/notification-handler.sh" + } + ] } ] } }Reference
Working example from
token-optimizer(v5.1.0) that uses the correct nested schema:{ "matcher": "Read", "hooks": [ { "type": "command", "command": "python3 '${CLAUDE_PLUGIN_ROOT}/skills/token-optimizer/scripts/read_cache.py' --quiet" } ] }