Plugin registry: project-scope rows re-pointed/consumed when sessions open in git worktrees — installs erode machine-wide
Environment: Claude Code 2.1.220, macOS (Darwin 25.5.0). Plugins distributed via a private git marketplace, installed at --scope project across ~9 repos. Heavy git-worktree workflow (each session opens in <repo>/.claude/worktrees/<name>).
Summary
Project-scope install rows in ~/.claude/plugins/installed_plugins.json are re-pointed to a different projectPath (rather than a new row being added) when sessions open at paths that have no matching row — most commonly git worktrees of repos whose checked-in .claude/settings.json sets enabledPlugins for the plugin. Over days of normal use the registry erodes: repos that had project-scope installs silently lose them, and with them every skill and every hook the plugin ships.
Observed behaviour
- A project-scope row's
projectPathwas observed rewritten twice in one afternoon while itsinstalledAtstayed constant (2026-07-28T18:14:47.506Zin my registry — same row, three homes):
- 11:23 →
<repo-A>(repo root) - 16:55 →
<repo-B>/.claude/worktrees/<wt-1>(a different repo's worktree) - 17:09 →
<repo-C>/.claude/worktrees/<wt-2>(a third repo's worktree, scope flipped tolocal)
- Registry erosion over one week: 7 project-scope rows for the plugin on 2026-07-30 → 5 on 2026-08-06 morning → 3 by that evening. A second plugin from a second marketplace eroded 3 rows → 1 the same way. No
claude plugin uninstallwas ever run and nothing edited the file directly (verified from session transcripts). - Mid-session effect: a session resolves plugin skills early (e.g. a
Skillcall succeeds at 13:37), another session starts elsewhere and consumes the row, and from that moment every skill from the plugin returnsUnknown skillin the first session (measured at 15:23 in the same transcript). The plugin cache on disk is intact throughout;claude plugin liststill shows the marketplace content. - Because worktree paths never match a repo root's project row, each new worktree session re-triggers the behaviour — the single wandering row follows the most recently opened session, and every other repo's next skill call fails.
Expected behaviour
- A session opened in a git worktree should resolve project-scope plugin installs via the repo the worktree belongs to (e.g. resolve through
git rev-parse --git-common-dir), instead of treating the worktree path as an unknown project. - If the app repairs an enabled-but-not-installed state, it should add a row scoped to that path, never re-point or consume an existing row that belongs to a different project.
Repro sketch
- Repo
Awith.claude/settings.json→"enabledPlugins": {"my-plugin@my-marketplace": true}; install withclaude plugin install my-plugin@my-marketplace --scope projectfromA's root. Repeat for repoB. git -C A worktree add .claude/worktrees/wt1 -b testand open a Claude Code session inA/.claude/worktrees/wt1.- Inspect
~/.claude/plugins/installed_plugins.json: a pre-existing row'sprojectPathis rewritten (observed with unchangedinstalledAt), rather than a new row appearing; repoB(orA's root) subsequently fails to resolve the plugin's skills, and the plugin's hooks stop loading there.
Impact
Teams distributing engineering standards as marketplace plugins lose skills and PreToolUse guardrail hooks silently, per repo, in an order that tracks which repos are worked on most. The failure surfaces as Unknown skill far from the cause, sometimes hours into a session. Workaround we've adopted: --scope user installs, which are path-independent and immune.