[BUG] Project-scoped plugin enabled in multiple repos never loads on first session after switching repos (single install record, re-stamped after plugin load)

Status Fixed / completed
Reported on v2.1.199
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jul 3, 2026 · closed Aug 19, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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:

  1. ~/.claude/plugins/installed_plugins.json keeps a single project-scoped entry per plugin, with one projectPath. Multi-project enablement is not representable.
  2. The plugin loader loads a project-scoped plugin only when record.projectPath === cwd.
  3. 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 projectPath to 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

  1. Two repos, project-a and project-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 }
}
``

  1. Install/trust the plugin from project-a (registry entry becomes {"scope": "project", "projectPath": ".../project-a"}).
  2. cd project-a && claude --debug-file /tmp/a1.log -p "ok" → log shows Loaded N skills from plugin my-plugin. ✅
  3. cd project-b && claude --debug-file /tmp/b1.log -p "ok" → no Loaded N skills from plugin my-plugin line, no plugin skills/agents/hooks in the session. ❌ Registry projectPath is now .../project-b.
  4. cd project-b && claude ... again → loads. ✅ (record now matches)
  5. 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 enabledPlugins does not stabilize it: with both user and project settings enabling the plugin, the sync's desired state resolves to project scope with projectPath = 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

View original on GitHub ↗

3 Comments

vpapaloukas · 3 months ago

Corroborating this on Windows, with a concurrent-sessions twist that I think is direct field evidence for your mechanism #3 (the sync re-stamping projectPath to 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

  • Claude Code on Windows 11 (win32; PowerShell primary, Git Bash available). Version: 2.1.210.
  • Private GitHub marketplace acme-platform (extraKnownMarketplaces → source: github, autoUpdate: true), enabled via a committed, project-level .claude/settings.json.
  • Multiple concurrent Claude Code sessions across sibling repos on the same machine (I saw 7 distinct PIDs in the .in_use markers under the plugin cache).

Two sibling repos, both with the same checked-in settings enabling three plugins:

{
  "extraKnownMarketplaces": {
    "acme-platform": {
      "source": { "source": "github", "repo": "<org>/<marketplace-repo>", "ref": "latest" },
      "autoUpdate": true
    }
  },
  "enabledPlugins": {
    "acme-core@acme-platform": true,
    "acme-devops@acme-platform": true,
    "acme-qa@acme-platform": true
  }
}

What happened

In project-a, acme-qa and acme-devops loaded, but acme-core never did — no commands, no skills, in any session; invoking one gave Unknown skill: acme-core:<cmd>. The difference was entirely in the shared ledger:

| plugin | install record(s) in installed_plugins.json | loaded in project-a? |
|---|---|---|
| acme-devops | scope: user (global) | ✅ |
| acme-qa | scope: project → project-b and → project-a | ✅ |
| acme-core | scope: project → project-b only (no project-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-platform record shared a single lastUpdated timestamp with projectPath pointing at project-b — written by a session running in project-b about an hour before the failing project-a session started. So your mechanism #3 fires from a concurrent process: a project-b session re-stamped the shared, single-entry records to projectPath: project-b, and acme-core's project-a entry was clobbered in the process. (acme-qa happened 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 /plugin panel reported acme-core as a cache miss:

❯ acme-core (project)
   Plugin "acme-core" not cached at C:\Users\<user>\.claude\plugins\cache\acme-platform\acme-core\1.27.0

That path was verified complete and valid (.claude-plugin/plugin.json parses; 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 misleading not 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 in installed_plugins.json") and the file. Possibly related path-form angle: on Windows my ~/.claude.json keys the project with forward slashes (C:/work/project-a) while installed_plugins.json uses 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-plugins masks it without persisting. /reload-plugins made acme-core work 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-platform wrote the missing project-a record (leaving the project-b record 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:

  • Make the startup message accurate — name the missing-install-record condition and the file, instead of reporting a cache miss for a valid cache.
  • Guard installed_plugins.json against concurrent cross-project writes (the parallel-session clobber above), or have /reload-plugins persist the record so the fix survives a cold start.

Environment

  • Claude Code 2.1.210 (native install), Windows 11 (win32)
  • Plugin: private GitHub marketplace, enabled via checked-in project settings in multiple sibling repos, with concurrent sessions running against a shared plugin cache
sevenbai · 2 months ago

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.json only (no checked-in
.claude/settings.json anywhere), but their installed_plugins.json records were all
scope: project with projectPath pinned to my home directory:

superpowers@claude-plugins-official  scope=project  projectPath=C:\Users\<me>  version=6.1.1
codex@openai-codex                   scope=project  projectPath=C:\Users\<me>  version=1.0.4
atlassian@claude-plugins-official    scope=project  projectPath=C:\Users\<me>

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-plugins 18 times across
14 sessions
without understanding why — the plugin is simply absent, with no message.

The version-drift symptom is visible too. Six consecutive --debug-file probes on
the same machine within the same minute, differing only in cwd:

03:39 / 03:50 / 03:56 / 03:57 / 03:59   cwd=<repo>   -> superpowers\3dcbd5c4b48e\skills   (v6.2.0)
03:59                                   cwd=<HOME>   -> superpowers\6.1.1\skills          (v6.1.1)

Same plugin, same minute, two different versions depending on where the session
started. The record's gitCommitSha (363923f7…) matched none of the on-disk cache
directories at that point.

Side effect worth noting: cache directories accumulate. Because each
/reload-plugins re-materialises the plugin while the stale record keeps producing
orphan marks, superpowers had 4 cache directories (2 of them carrying .orphaned_at)
totalling ~7 MB for one plugin. I also observed an .orphaned_at marker being stamped
on a directory that still had a live session's PID in its .in_use/ — I can't yet prove
that 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 projectPath gate entirely.

claude plugin install   <plugin> -s user
claude plugin uninstall <plugin> -s project --keep-data -y
claude plugin enable    <plugin> -s user     # <- REQUIRED, see below

That third line is not optional: plugin uninstall -s project deletes the
enabledPlugins["<plugin>"] key from ~/.claude/settings.json outright, which
silently 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:

claude --debug-file /tmp/pl.log -p "ok" < /dev/null > /dev/null 2>&1
grep -E 'Found [0-9]+ plugins|plugin-cache-miss|not cached' /tmp/pl.log

The complete absence of any user-facing signal is the worst part — claude plugin list
reports the plugin as Status: enabled and claude plugin details happily enumerates
all of its skills while the loader is skipping it.

bcherny collaborator · 1 month ago

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

Showing cached comments. Read the full discussion on GitHub ↗