[BUG] Project-scope plugin install keys to the session cwd, not the project root (leads silent duplicates, stale plugin versions loading)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Jul 31, 2026

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?

claude plugin install --scope project records the session cwd as the project, rather than the project root. Three things follow from that, all measured on 2.1.220.

First, the install record and the settings file land in whatever directory you happened to be in. Installing from <repo>/a writes a record with projectPath: <repo>/a and creates <repo>/a/.claude/settings.json. In a monorepo that scatters plugin config into package directories, and it quietly sidesteps a repo's committed root .claude/settings.json.

Second, installing again from a different directory in the same repo silently creates a second record. No "already installed" message, no prompt, nothing. Do it from a third directory and you get a third record. Nothing anywhere reports that one project now has several competing records for one plugin.

Third - and I think this is the worst of the three - uninstalling from a different directory fails, and the error misreports the scope. With a project-scope record at <repo>/a, running claude plugin uninstall <plugin> --scope project from the repo root gives:

Failed to uninstall plugin "<plugin>": Plugin "<plugin>" is installed in user scope, not project. Use --scope user to uninstall.

That is wrong. The plugin is installed at project scope; it's just recorded against a subdirectory. Following the advice would uninstall the user-scope copy instead, which is a different install the user never asked to remove. Meanwhile the project record survives, so an uninstall-then-reinstall intended to reset the plugin leaves two records instead of one.

The thing that ties these together, and the clearest statement of the bug I can give: the same path relationship is treated as a match by the resolver and as a non-match by uninstall. One product, two opposite answers.

Once two records exist, one of them silently wins and the other is dead weight - but there's no way to see which is live, and no reliable way to remove the stale one. In my case a plugin ran four releases behind for nine days across ~10 dispatched subagent runs, with a different set of tool grants than its definition specified. Everything reported as installed and up to date throughout.

For what it's worth, the product already resolves a repository root elsewhere - the 2.1.218 changelog describes trust dialogs naming "the repository root the grant covers" - so there seem to be two different notions of "project" in play.

What Should Happen?

A project-scope install should key to the project root, the same root workspace trust already resolves, so that installing from anywhere within a project gives one record and one settings file.

Installing a plugin that's already installed for the same project should update it, or say it's already installed. It shouldn't silently add a competing record.

Uninstalling at project scope should find the plugin's record(s) from anywhere within that project. If it genuinely can't find one, the error mustn't claim the plugin is user-scoped, because acting on that advice removes a different install.

Error Messages/Logs

Steps to Reproduce

Verified on 2.1.220, macOS. Any plugin reproduces it; the output below is from swift-lsp@claude-plugins-official in a throwaway git repo.

  1. Create a git repo at <repo> with a subdirectory a
  2. From <repo>/a: claude plugin install <plugin> --scope project
  3. From <repo>: claude plugin uninstall <plugin> --scope project
  4. From <repo>: claude plugin install <plugin> --scope project

Reading ~/.claude/plugins/installed_plugins.json after each step:

| Step | Result | Records for the plugin |
|---|---|---|
| 2 | "Successfully installed (scope: project)" | user-scope, plus project-scope at <repo>/a |
| 3 | Fails: "is installed in user scope, not project. Use --scope user" | unchanged; the <repo>/a record survives |
| 4 | "Successfully installed (scope: project)" | user-scope, plus project at <repo>/a, plus project at <repo> |

Step 2 also creates <repo>/a/.claude/settings.json; step 4 creates a second one at <repo>/.claude/settings.json.

There's a shorter variant with no uninstall at all: install from <repo>/a, then <repo>/b, then <repo>. That gives three project-scope records for one plugin in one repo, and three separate .claude/settings.json files. Every install reports success and none of them mentions the others.

One thing that does behave correctly, which I mention because it narrows where the fix belongs: claude plugin update --scope project run from a directory with no record of its own locates and updates the existing record rather than creating a new one. So install and update disagree about what a project is.

The only error is the misleading one at step 3:

✘ Failed to uninstall plugin "swift-lsp@claude-plugins-official": Plugin "swift-lsp@claude-plugins-official" is installed in user scope, not project. Use --scope user to uninstall.

Everything else is silent. claude plugin list shows every record as enabled, with nothing to tell them apart:

❯ myplugin@mymarketplace
  Version: 0.9.0
  Scope: project
  Status: ✔ enabled

❯ myplugin@mymarketplace
  Version: 0.13.0
  Scope: project
  Status: ✔ enabled

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.220

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Suggested fixes:

  • key project scope to the project root rather than the session cwd, matching how workspace trust already resolves the repository root; this prevents the duplicate records rather than having to resolve between them afterwards, and it also puts .claude/settings.json somewhere a repo can commit it
  • on install, detect an existing record for the same project and update it, or report that it's already installed
  • make uninstall --scope project use the same matching as resolution, so it can find a record registered against any directory in the project; failing that, at minimum fix the error message so it never advises uninstalling from a scope the user didn't target
  • show which record is live in claude plugin list; nothing currently distinguishes an active record from a shadowed one, and that's most of why this took days rather than minutes to pin down

I'd put the first of those as the root fix. If project scope keyed to the project root, none of the rest of this could arise.

---

Also worth noting that there is a cluster of related-but-distinct issues:

#79892 covers duplicate records arising from --scope user installs against a malformed record, and the first-match resolution that makes duplicates harmful; I've commented there with a repro.

#81058 covers uninstall -s project deleting the enabledPlugins key - same command as one of my findings, opposite failure.

#81706 covers dual-scope enablement producing a project-only record.

#73796 covers a single project record ping-ponging between repos, and states a single-record-per-plugin premise that no longer holds on 2.1.220; I've noted that there.

None of these covers install keying to the session cwd, silent per-directory duplicates, or the uninstall misreport.

View original on GitHub ↗

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