Plugin hooks load additively from both plugin.json and hooks/hooks.json, contradicting docs (2.1.159)

Status Open
Reported on v2.1.159
Maintainer reply None cached
Activity 0 comments · opened Aug 21, 2026

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 in claude plugin list and the /plugin detail 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:

  1. claude plugin details counts hooks by event, not by handler. With the same event declared in both locations it reports Hooks (1) SessionStart, which reads like override behaviour and is not. Two handlers on one event are indistinguishable from one.
  2. claude plugin validate passes a double declaration cleanly — it returned only an unrelated author warning.

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 validate warning 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.

View original on GitHub ↗