extraKnownMarketplaces (directory source): global registry entry silently overwritten by identically-named copy of the repo (e.g. git submodule)
Summary
A project-scoped extraKnownMarketplaces declaration with a directory source is persisted into the global registry (~/.claude/plugins/known_marketplaces.json) keyed by name only. Opening a trusted copy of the same repository — including its own checkout mounted as a git submodule inside another project — silently overwrites the registered origin for every other project on the machine (last-writer-wins, no warning).
Environment
- Claude Code 2.1.220, macOS (darwin 25.5.0)
Reproduction
- Repo
corecontains, versioned:
.claude-plugin/marketplace.jsonwith"name": "my-market"and a plugin under./plugins/....claude/settings.jsonwith:
``json``
{
"extraKnownMarketplaces": {
"my-market": { "source": { "source": "directory", "path": "." } }
},
"enabledPlugins": { "myplugin@my-market": true }
}
- Repo
consumermountscoreas a pinned git submodule atconsumer/core(a byte-identical copy of both files above, detached HEAD). - Start any session in the real
corecheckout → registry entrymy-marketpoints at the real checkout. Expected. - Trust the folder
consumer/coreand run a headless session there (cd consumer/core && claude -p "ok"). - Inspect
~/.claude/plugins/known_marketplaces.json: themy-marketentry now points atconsumer/core— the stale, pinned submodule copy. No warning was emitted; nothing indicates the slot changed owner. - Start a session in the real
coreagain → the entry is silently overwritten back. The two checkouts fight over the slot indefinitely, last boot wins.
Also observed (correct/expected behavior worth keeping): if the submodule folder is not trusted, its settings are ignored and no overwrite happens.
Impact
- Sessions in the real repo can silently load plugin/skill content from an outdated pinned copy that happens to have booted last.
- Any tooling that resolves the marketplace's
source.pathfrom the registry (e.g. maintenance scripts runningclaude plugin marketplace update/claude plugin update) operates on the wrong checkout — in our case a detached-HEAD submodule — with no signal other than downstream symptoms. - The relative
path: "."pattern is the natural way to make a repo self-registering and portable, which makes this collision easy to hit for anyone distributing a plugin repo as a submodule.
Expected behavior (any of these would resolve it)
- Scope the effective registration to the declaring project, rather than materializing a single global slot keyed by name.
- Or refuse (or at least warn) when an
extraKnownMarketplacesdeclaration resolves to a different source than an existing same-name entry, instead of silently overwriting. - Or key registry identity by name + declaring project, instead of last-writer-wins by name alone.
- Or provide an explicit reconciliation command so scripts can pin the canonical origin without editing settings or depending on the global slot.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗