Plugin hooks load additively from both plugin.json and hooks/hooks.json, contradicting docs (2.1.159)
Product: Claude Code CLI 2.1.159
The claim in the docs
code.claude.com/docs/en/plugins-reference says a plugin's hooks live at "hooks/hooks.json in plugin root, or inline in plugin.json", and then states the precedence rule explicitly:
When a plugin has both a default folder and the matching manifest key, Claude Code v2.1.140 and later warns about the ignored folder inclaude plugin listand the/plugindetail view. The plugin still loads using the manifest paths.
Read plainly: declare hooks in both places and the manifest wins, the folder is ignored, and you get a warning.
What actually happens
All three parts are wrong for hooks on 2.1.159. Both sources load, both handlers run, and there is no warning.
Reproduction
Build a plugin declaring the same event in both locations, with a distinguishable side effect in each:
.claude-plugin/plugin.json
{
"name": "hooktest3",
"version": "0.1.0",
"description": "probe",
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "echo INLINE >> /tmp/fired.log" }
]
}
]
}
}
hooks/hooks.json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "echo FILE >> /tmp/fired.log" }
]
}
]
}
}
Then:
claude --plugin-dir ./hooktest3 -p "Reply with exactly: OK"
cat /tmp/fired.log
Expected per docs: one line (INLINE), plus a warning about the ignored hooks/ folder.
Actual:
INLINE
FILE
Both fired. No warning was emitted by claude plugin list, claude plugin validate, or claude plugin details.
Why this matters
A plugin author who declares hooks in both places — reasonably, having read that the manifest overrides — gets every hook running twice. For a hook with a side effect that is not idempotent, that's a real defect (e.g. a test-running hook running the whole suite twice per turn, a deny-listing hook denying twice, a state-snapshot hook writing its file twice).
The failure is silent. Nothing in the tooling flags it.
Two secondary findings
Neither of these is a bug on its own, but both hide the problem:
claude plugin detailscounts hooks by event, not by handler. With the same event declared in both locations it reportsHooks (1) SessionStart, which reads like override behaviour and is not. Two handlers on one event are indistinguishable from one.claude plugin validatepasses a double declaration cleanly — it returned only an unrelatedauthorwarning.
So the natural ways to check "did I configure this twice?" both say no.
Suggested fix
Either the behaviour or the documentation is wrong, and the fix differs:
- If additive is intended — correct the plugins reference, which currently promises override for the folder-plus-manifest-key case, and consider a
validatewarning when a plugin declares the same event in both locations. - If override is intended — hooks are not honouring the documented rule that other folder-backed components follow.