[BUG] Project-scoped plugin enabled in multiple repos never loads on first session after switching repos (single install record, re-stamped after plugin load)
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 (2.1.199)
What's Wrong?
When the same plugin is enabled via checked-in .claude/settings.json (extraKnownMarketplaces + enabledPlugins) in two or more repos, the plugin silently fails to load in the first session after every repo switch — all of its skills, agents, hooks, and MCP servers are absent. If you alternate between the repos (the normal workflow the team-onboarding feature is designed for), the plugin effectively never loads.
Three interacting behaviors cause this:
~/.claude/plugins/installed_plugins.jsonkeeps a single project-scoped entry per plugin, with oneprojectPath. Multi-project enablement is not representable.- The plugin loader loads a project-scoped plugin only when
record.projectPath === cwd. - The startup sync ("Syncing installed_plugins.json with enabledPlugins from all settings.json files" / "settings.json is source of truth") re-stamps the record's
projectPathto the current project — but it runs after plugins have already been loaded.
So a session in repo B finds the record pointing at repo A → skips the plugin → then "repairs" the record to B, which breaks the next session in repo A. Ping-pong.
Steps to Reproduce
- Two repos,
project-aandproject-b, each with an identical checked-in.claude/settings.json:
``json``
{
"extraKnownMarketplaces": {
"my-marketplace": { "source": { "source": "github", "repo": "org/claude-plugin" } }
},
"enabledPlugins": { "my-plugin@my-marketplace": true }
}
- Install/trust the plugin from
project-a(registry entry becomes{"scope": "project", "projectPath": ".../project-a"}). cd project-a && claude --debug-file /tmp/a1.log -p "ok"→ log showsLoaded N skills from plugin my-plugin. ✅cd project-b && claude --debug-file /tmp/b1.log -p "ok"→ noLoaded N skills from plugin my-pluginline, no plugin skills/agents/hooks in the session. ❌ RegistryprojectPathis now.../project-b.cd project-b && claude ...again → loads. ✅ (record now matches)cd project-a && claude ...→ missing again. ❌
Evidence from --debug-file
Ordering inside a single startup — load happens before the sync that would have fixed the record:
[DEBUG] Loaded 32 installed plugins from ~/.claude/plugins/installed_plugins.json
[DEBUG] Attempting to load skills from plugin my-plugin default skillsPath: ... <-- gated by stale projectPath
[DEBUG] Loaded 28 skills from plugin my-plugin <-- only when projectPath matches cwd
[DEBUG] Syncing installed_plugins.json with enabledPlugins from all settings.json files <-- runs AFTER load
[DEBUG] Updated my-plugin@my-marketplace scope to project (settings.json is source of truth)
The re-stamp is observable as lastUpdated/projectPath changing in installed_plugins.json at session start, one session too late.
Notes
- User-level
enabledPluginsdoes not stabilize it: with both user and project settings enabling the plugin, the sync's desired state resolves toprojectscope withprojectPath = cwd, so the record keeps flipping. - Manually adding two project entries to the registry array (one per repo) survives sessions in the repo whose entry is first, but a session in the other repo rewrites the first entry to the current project and the dedupe pass collapses the now-identical entries back to one.
- Related but distinct: #45997 (closed) — loader taking
[0]from a multi-entry array; here the sync actively collapses to a single entry so the wrong-version symptom becomes a not-loaded-at-all symptom. #63663 (open, "enabledPlugins not persisting across restarts") looks like the same root cause observed without the multi-repo diagnosis.
Expected Behavior
A plugin enabled by the current project's settings should load regardless of which project a shared single install record was last stamped with. Either keep one install record per (plugin, projectPath), or run the settings→registry sync before plugin loading, or have the loader treat "enabled in current project's settings + present in cache" as loadable.
Environment
- Claude Code 2.1.199 (native install), macOS 14 (arm64)
- Plugin: private GitHub marketplace, enabled via checked-in project settings in 3 repos
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Corroborating this on Windows, with a concurrent-sessions twist that I think is direct field evidence for your mechanism #3 (the sync re-stamping
projectPathto the current project). In my case the re-stamp came from a parallel session in another repo, not from a sequential repo switch — same outcome, harder to spot.Setup
win32; PowerShell primary, Git Bash available). Version:2.1.210.acme-platform(extraKnownMarketplaces→source: github,autoUpdate: true), enabled via a committed, project-level.claude/settings.json..in_usemarkers under the plugin cache).Two sibling repos, both with the same checked-in settings enabling three plugins:
What happened
In
project-a,acme-qaandacme-devopsloaded, butacme-corenever did — no commands, no skills, in any session; invoking one gaveUnknown skill: acme-core:<cmd>. The difference was entirely in the shared ledger:| plugin | install record(s) in
installed_plugins.json| loaded inproject-a? ||---|---|---|
|
acme-devops|scope: user(global) | ✅ ||
acme-qa|scope: project→project-band →project-a| ✅ ||
acme-core|scope: project→project-bonly (noproject-a) | ❌ |The one plugin with no record for the current project is the one that didn't load — while enabled, and while fully present in the cache.
The re-stamp is observable cross-process, not just on repo switch
Every
acme-platformrecord shared a singlelastUpdatedtimestamp withprojectPathpointing atproject-b— written by a session running inproject-babout an hour before the failingproject-asession started. So your mechanism #3 fires from a concurrent process: aproject-bsession re-stamped the shared, single-entry records toprojectPath: project-b, andacme-core'sproject-aentry was clobbered in the process. (acme-qahappened to retain both entries and survived — consistent with your note that the dedupe pass collapses multi-entry records back to one, non-deterministically depending on which session writes last.)Two things worth folding into this report
1. The startup error actively points at the wrong place. The
/pluginpanel reportedacme-coreas a cache miss:That path was verified complete and valid (
.claude-plugin/plugin.jsonparses; 12 commands / 1 skill / 3 hooks present). The real condition was a missing install record for this project, but the message says "not cached" for a cache that is fine — it sends you to debug the cache instead of the ledger. The same misleadingnot cached at <path>string shows up in #74912 with a different underlying cause (drive-letter case), which suggests the message is a catch-all that should instead name the real condition ("no install record for this project ininstalled_plugins.json") and the file. Possibly related path-form angle: on Windows my~/.claude.jsonkeys the project with forward slashes (C:/work/project-a) whileinstalled_plugins.jsonuses escaped backslashes (C:\\work\\project-a) — a normalization mismatch that could make a project-scoped record fail to match at startup (cf. #74912, #75855).2.
/reload-pluginsmasks it without persisting./reload-pluginsmadeacme-corework for the rest of the session but wrote nothing to the ledger, so the next cold session was broken again — which is exactly why this read as a flaky transient for weeks rather than a permanent misconfiguration./plugin install acme-core@acme-platformwrote the missingproject-arecord (leaving theproject-brecord intact), and a subsequent cold session loaded it with no/reload-plugins. So the persistent fix confirms the record — not the cache — was the problem, and confirms there is no self-heal on startup (the ledger was byte-identical after a fresh failing session).On the fix options
Strong +1 to "run the settings→registry sync before plugin loading" or "keep one record per (plugin, projectPath)". I'd add two smaller asks that would have saved most of the diagnosis even without the structural fix:
installed_plugins.jsonagainst concurrent cross-project writes (the parallel-session clobber above), or have/reload-pluginspersist the record so the fix survives a cold start.Environment
2.1.210(native install), Windows 11 (win32)Independent confirmation on 2.1.220 / Windows 11, and a data point on how this looks
when the "two repos" are just any two working directories rather than two repos with
checked-in settings.
Setup: plugins enabled via user-level
~/.claude/settings.jsononly (no checked-in.claude/settings.jsonanywhere), but theirinstalled_plugins.jsonrecords were allscope: projectwithprojectPathpinned to my home directory:I work across ~10 directories (a main repo plus git worktrees), so the ping-pong you
describe fires constantly. Over a 9-day window I ran
/reload-plugins18 times across14 sessions without understanding why — the plugin is simply absent, with no message.
The version-drift symptom is visible too. Six consecutive
--debug-fileprobes onthe same machine within the same minute, differing only in cwd:
Same plugin, same minute, two different versions depending on where the session
started. The record's
gitCommitSha(363923f7…) matched none of the on-disk cachedirectories at that point.
Side effect worth noting: cache directories accumulate. Because each
/reload-pluginsre-materialises the plugin while the stale record keeps producingorphan marks, superpowers had 4 cache directories (2 of them carrying
.orphaned_at)totalling ~7 MB for one plugin. I also observed an
.orphaned_atmarker being stampedon a directory that still had a live session's PID in its
.in_use/— I can't yet provethat the in-use sweep then reaps it, so I'm noting it only as an observation, not a
claim.
Workaround that stabilised it here (in case it helps others landing on this issue):
converting the records to user scope removes the
projectPathgate entirely.That third line is not optional:
plugin uninstall -s projectdeletes theenabledPlugins["<plugin>"]key from~/.claude/settings.jsonoutright, whichsilently disables the user-scope install you just made. Filed separately as #81058.
After converting to user scope, both cwds resolve to the same directory and the plugin
loads every time.
Detection recipe for anyone unsure whether they're hitting this:
The complete absence of any user-facing signal is the worst part —
claude plugin listreports the plugin as
Status: enabledandclaude plugin detailshappily enumeratesall of its skills while the loader is skipping it.
Fixed in 2.1.224 — changelog: "Fixed plugin install records being silently corrupted when the same plugin is installed in multiple projects" (https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md). Closing — reply to reopen if it still happens on the latest version.
🤖 Generated with Claude Code