[BUG] `claude plugin install --scope project` overwrites `installed_plugins.json` instead of merging
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?
Each call to claude plugin install <plugin> --scope project overwrites ~/.claude/plugins/installed_plugins.json with a fresh single-entry file, erasing all previously registered plugins. After installing N plugins sequentially, only the last one survives in the registry.
The Plugins UI Errors tab then reports every project-scoped plugin as _"enabled in project settings but isn't installed here"_. Attempting to fix this by re-running claude plugin install for each missing plugin makes it worse — each fix wipes the entries installed before it.
What Should Happen?
claude plugin install should read the existing installed_plugins.json, merge the new plugin entry into the existing "plugins" object, and write the result back. All previously installed plugins should remain registered after installing a new one.
Error Messages/Logs
No explicit error is shown. The only observable signals are:
- The Plugins UI Errors tab shows N errors (one per project-scoped plugin, except the last-installed)
- Each error reads: `Plugin "X" is enabled in project settings but isn't installed here`
- Running `claude plugin install X --scope project` reports `✔ Successfully installed` but silently removes all other entries from the registry
Steps to Reproduce
- Install two or more plugins at project scope sequentially:
````
claude plugin install plugin-a@marketplace --scope project
claude plugin install plugin-b@marketplace --scope project
- Inspect
~/.claude/plugins/installed_plugins.json - Observe that only
plugin-bis present —plugin-a's entry has been overwritten
Additional context: Discovered after brew upgrade claude to 2.1.195. Plugins installed before the upgrade were correctly present in installed_plugins.json. The overwrite behavior began with the first claude plugin install run after the upgrade.
Claude Model
Not sure / Multiple models
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.195 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
The plugin cache files under ~/.claude/plugins/cache/ are not affected — all plugin content is correctly downloaded. Only the installation registry (installed_plugins.json) is corrupted by the overwrite.
Workaround: Manually reconstruct ~/.claude/plugins/installed_plugins.json by merging all entries:
{
"version": 2,
"plugins": {
"plugin-a@marketplace": [
{
"scope": "project",
"installPath": "~/.claude/plugins/cache/marketplace/plugin-a/<version>",
"version": "<version>",
"installedAt": "<timestamp>",
"lastUpdated": "<timestamp>",
"gitCommitSha": "<sha>",
"projectPath": "/path/to/project"
}
],
"plugin-b@marketplace": [ "..." ]
}
}
Note: The direct consequence of this is installing skills only to see the older skills with errors, next. Trying to follow the instruction to reinstall the plugins via cli does not work, since installing one uninstall the others.
Showing cached comments. Read the full discussion on GitHub ↗
7 Comments
Please, if anyone could check this. Skills obtained through a custom marketplace are basically unusable at this point.
We're hitting what looks like the same non-merging registry write, but through an implicit trigger — no
claude plugin installcommand involved — and with a different blast radius. Sharing because it suggests the defective write path fires far more broadly than the CLI repro here.Environment: Claude Code 2.1.207, macOS (Darwin 25.5), plugins from git marketplaces with
autoUpdate: true, enabled viaenabledPluginsin each repo's.claude/settings.json.Symptom: a session that uses a plugin (e.g. invokes one of its skills) rewrites the
~/.claude/plugins/installed_plugins.jsonentries for every plugin the current project enables — each per-plugin array is replaced wholesale with a single{"scope": "project", "projectPath": "<session cwd>"}entry. Existing"scope": "user"entries are clobbered rather than merged/appended. Net effect on a machine where many repos enable the same plugins: whichever repo last ran a plugin-using session "owns" them, and sessions in every other repo fail to load them (Unknown skill: <name>,agent type '<plugin>:<agent>' not found) — whileclaude plugin liststill shows them as installed + enabled from any cwd.Repro (observed, deterministic on the affected machine):
claude plugin install codex@skills --scope user→ registry entry is{"scope": "user"}. ✅.claude/settings.jsonhas"enabledPlugins": {"codex@skills": true, ...}), run a session that invokes the plugin's skill, e.g.claude -p --permission-mode bypassPermissions "invoke the codex skill ...".codex@skills— and for all other plugins repo A enables (six in our case, including ones the session never used) — now reads{"scope": "project", "projectPath": "/path/to/repoA"}. The user-scope entries are gone.Additional observations:
claude -p "say hi", no tool use) do not trigger the rewrite; plugin-using sessions do. Consistent with the docs note that as of v2.1.195 the install/registration step "applies on every path that loads plugins" (discover-plugins §7) — same defective write, implicit trigger.project+userentries for the same plugin), so the clobber may depend on registry state/timing when the implicit registration pass decides to write.pluginDefaultScope) was closed stale, andautoUpdate: falseinknown_marketplaces.jsonis re-asserted from settingsextraKnownMarketplaceson the next load. We're running an external watcher that restores the user-scope entries after each clobber, which is not a workaround that should be necessary.Related: #14202, #62209, and the v2.1.108 changelog entry ("Fixed policy-managed plugins never auto-updating when running from a different project than where they were first installed"), which made this reconciliation run from any project.
Issue remains in version
2.1.206.@yasyf Bug has been demonstrated with the cli, but it is also reproducible via the claude-code interactive session.
Each new session will overwrite a previous
installed_plugins.jsonwith the newly installed plugins in that session.It is seems that the current logic assumes that the user will install every needed plugin at once, in a single session, and that there is a single project in the system, which is absurd. This cannot possibly be what developers had in mind.
Leveraging custom marketplaces via plugins continue unusable at this point.
I am seeing the same problem at the
userlevel.While trying to circumvent the issue in the
projectscope, I noticed that theuserlevel installation is also overwriting the json config file in the system.This overall behavior is a critical blocker for marketplace users.
Since the
claude-codeCLI is proprietary (closed-source), direct PR contributions to the CLI itself are not possible. I am sharing an analysis of the observed behavior and a suggested TypeScript implementation to help the Anthropic team resolve the issue.Root Cause Analysis (inferred from observed behavior)
In
claude-code>= 2.1.195, both the CLI installer and the implicit plugin-loading path register active plugins in the registry file~/.claude/plugins/installed_plugins.json.The registry maps each plugin ID (e.g.
codex@skills) to an array of installation records. The array exists because the same plugin can legitimately be active in multiple scopes at once — at theuserlevel globally, and at theprojectlevel for one or more project paths.The observed behavior is consistent with a registry write that replaces instead of merging — either replacing the per-plugin array with a single-element array (
registry.plugins[pluginId] = [newRecord]), or rebuilding the entire registry from only the plugins known to the current operation. Since the CLI is closed-source this is an inference, but it explains both reported failure modes:claude plugin install --scope projectruns wipe previously installed entries — only the last-installed plugin survives.{"scope": "project", "projectPath": "<cwd>"}record, clobbering the existing"scope": "user"record — after which every other project fails to load the plugin (Unknown skill: <name>), whileclaude plugin liststill claims it is installed.Requirements for a Correct Fix
A correct fix must satisfy all four of these, not just the first:
user-scope record replaces only the previoususerrecord; a newproject-scope record replaces only the record with the sameprojectPath. Records for other scopes/projects are appended-to, never dropped, so this state is valid and stable:``
json
``"codex@skills": [
{ "scope": "user", ... },
{ "scope": "project", "projectPath": "/path/to/repoA", ... },
{ "scope": "project", "projectPath": "/path/to/repoB", ... }
]
installed_plugins.json.corrupt-<timestamp>) — not silently replaced with an empty registry. A fallback that discards the file reintroduces this very bug through a different door.Suggested TypeScript Implementation
The same merge discipline must be applied at every write site of
installed_plugins.json— the explicitclaude plugin installpath, the implicit session-time registration/reconciliation path, and (with inverted logic: remove one record, keep the rest) the uninstall path. Fixing only the installer would leave @yasyf's implicit-clobber path intact.Also submitted via
/feedbackin Claude Code for internal correlation:Feedback ID
94fd108d-092a-4a0a-824b-fd4c67929755