[BUG] Project-scoped plugin records overwritten across projects (installed_plugins.json reconcile rewrites the first record instead of upserting by projectPath)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When the same plugin@marketplace is enabled at project scope by two or more projects that live at different paths — this includes both independent repositories that each ship the plugin in their committed .claude/settings.json, and (very commonly) two checkouts of the same repository at different absolute paths — the single global ~/.claude/plugins/installed_plugins.json cannot retain both install records.
On session start, the settings→installed_plugins.json reconciliation appears to rewrite an existing project-scoped record in place to point at the currently-open project, rather than keeping a separate record per project path. A follow-up dedup then discards the collision. Net effect: project-scoped plugin "homes" ping-pong between projects — whichever project you opened most recently owns the record, and the others report the plugin as errored / not-installed until their own next reconcile.
This is inconsistent with how records are otherwise handled: the applicability/read path and the installer both key on the project path (scope + projectPath), and the on-disk schema stores an array of records per plugin — so multiple project homes are representable everywhere except in this reconciliation step.
Related: #83758 — same root-cause family (global, project-path-keyed state breaking across multiple checkouts of one repository), different file (~/.claude.json / auto-memory vs installed_plugins.json).
What Should Happen?
Each project that enables a plugin at project scope should keep its own (project, <that project path>) install record. Opening one project must not mutate or delete another project's record. (A likely direction: have the reconciler upsert the record matching the current project path instead of editing the first record in the array — bringing it in line with the read path and installer, which already key on project path.)
Error Messages/Logs
No crash. Observable symptoms:
~/.claude/plugins/installed_plugins.json: a project-scoped plugin'sprojectPathflips to the current project each time you switch projects; the previous project's record is gone (not added alongside).- Running
/pluginin the project that just lost its record shows its project-scoped plugins in an errored/not-loaded state until that project re-reconciles (a fresh session there, or/reload-plugins).
Verified before/after on 2.1.221 — all project-scoped records flipped from one checkout's path to the other's, and the record count per plugin stayed at 1 (overwrite in place, not append):
BEFORE (session opened in checkout A): <plugin> → [{ scope: project, projectPath: .../checkout-A }] (1 record)
AFTER (session opened in checkout B): <plugin> → [{ scope: project, projectPath: .../checkout-B }] (1 record)
Plugins that also had a user-scope record kept both (a user record has no projectPath to collide on), consistent with the described mechanism.
Steps to Reproduce
- Two working directories at different absolute paths — either two independent repos, or two checkouts/worktrees of the same repo (
~/work/repoA,~/work/repoB). - Each commits the same entry in
.claude/settings.json:
``json``
{ "enabledPlugins": { "code-review@claude-plugins-official": true } }
- Open Claude Code in
repoA; observe ininstalled_plugins.json:
code-review@… → [{ scope: "project", projectPath: ".../repoA" }].
- Open Claude Code in
repoB; the same single record is now
[{ scope: "project", projectPath: ".../repoB" }] — the repoA record is gone, not added alongside.
- Back in
repoA, run/plugin: its project-scoped plugins report errors untilrepoAre-reconciles.
Impact: This is not an edge configuration. It triggers for any developer who (a) keeps multiple checkouts/worktrees of one repo (a routine practice), or (b) works across several independent repos that each enable a common plugin (formatters, code-review, commit-commands, etc.) via committed project settings. Project scope specifically exists to confine heavy plugins — browser-driving MCP servers, multi-skill bundles, language servers — to the few repos that need them; the usual "enable it at user scope" workaround defeats that by loading the plugin's full cost into every project.
Is this a regression?
Not sure.
Claude Code Version
2.1.221
Platform
Claude Code CLI (terminal), native install
Operating System
macOS
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗