Plugin hooks.json registrations multiply additively (1×→122×) in multi-agent sessions (v2.1.156)

Resolved 💬 1 comment Opened May 30, 2026 by MrJarrad Closed Jun 1, 2026

Summary

In multi-agent sessions, a plugin's hooks/hooks.json registrations multiply additively — each command registration object is duplicated N times, so every hook fires N times per tool call. Observed growth: 1× → 122× for a directory-source plugin. At high multiples the duplicated hook execution pollutes tool output with garbled content.

Environment

  • Claude Code 2.1.156
  • macOS (darwin 25.4.0)
  • Plugin installed from a local/directory-source marketplace ("type": "local", "source": "./"). The plugin is registered both as a directory source and as a cached install; both start at the canonical registration count.

What happens

  1. hooks/hooks.json starts at its canonical count (one registration object per hook).
  2. During a session that dispatches sub-agents (the Agent tool / Subagent lifecycle), the working-tree hooks.json grows — registration objects are duplicated additively (observed up to 122× per hook).
  3. git checkout -- hooks/hooks.json restores it, but it can re-grow.
  4. At high multiples, hooks fire N times each and tool output is corrupted.

The growth is purely additive (duplicated registration objects), consistent with the plugin-hook load/merge path re-running and appending the plugin's hook set rather than replacing it.

Why this looks like the harness, not the plugin

A full audit of the plugin's own scripts found no additive writer to hooks.json — the only writer overwrites the file with the canonical copy (atomic tmp+mv). The only mechanism that fits a 1×→N× additive growth is the harness re-running its plugin-hook load/merge and appending.

Reproduction status

Intermittent. It did not reproduce in a focused session (held at the canonical count across a no-op sub-agent dispatch + two parallel real-work agents + ~14 tool calls). No deterministic minimal repro yet — filing with the negative-reproduction record rather than a guaranteed repro, in case others can correlate.

Correct metric

Count structurally, not by line-grep:

jq '[.. | objects | select(.type=="command")] | length' hooks/hooks.json

(grep -c command double-counts — the "type" line plus the "command" line.)

Ask

  1. Is the plugin-hook load/merge expected to be idempotent across sub-agent dispatch / plugin reload? A duplicated-registration guard (or replace-not-append semantics) on the merge path would prevent this.
  2. Any known trigger for plugin reload mid-session that would re-run the merge?

Happy to add more detail or test a patch.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗