Plugin caching: `fallback to full copy` drops `entry.skills` + `Removing old cached version` wipes sibling plugins (url-source, name collision)
Summary
Two related bugs in plugin caching that surface together when a marketplace plugin uses url-based source and the plugin name equals the marketplace name (which is the natural pattern for "single-plugin marketplaces" where the repo itself is the plugin):
fallback to full copybranch dropsentry.skills— plugin loads with zero skills, slash commands disappear.Removing old cached versiondeletes a shared parent directory, wiping other plugins of the same marketplace whose caches happen to live as siblings.
Environment
- Claude Code: 2.1.123 (native installer, macOS Darwin 25.5.0)
Reproduction (100% deterministic, single instance, clean state)
Marketplace kb-catchtable declares two plugins (one url-based, one path-based) — multi-plugin marketplace, but the url-based plugin's name equals the marketplace name:
{
"name": "kb-catchtable",
"plugins": [
{ "name": "kb-catchtable",
"source": { "source": "url", "url": "https://github.com/CatchTable/kb-catchtable.git" } },
{ "name": "wip",
"source": "./plugins/wip" }
]
}
Steps:
claude /plugin marketplace add CatchTable/kb-catchtableclaude /plugin install kb-catchtable@kb-catchtable(path-based wip is auto-installed too)- Observe debug log
Debug log (relevant lines, single Claude Code instance, freshly cleared cache and installed_plugins.json):
[DEBUG] Caching plugin from source: {"source":"url","url":".../kb-catchtable.git"} to temporary path .../temp_git_<X>
[DEBUG] Copying source directory ./plugins/wip for plugin wip@kb-catchtable
[DEBUG] Successfully cached plugin wip@kb-catchtable at .../cache/kb-catchtable/wip/0.1.0 ← wip OK
[DEBUG] Copied plugin wip to versioned cache: .../cache/kb-catchtable/wip/0.1.0
[DEBUG] Cloned repository from .../kb-catchtable.git to .../temp_git_<X>
[DEBUG] Removing old cached version at .../cache/kb-catchtable ← BUG #2
[DEBUG] Successfully cached plugin kb-catchtable to .../cache/kb-catchtable
[DEBUG] Using git SHA for kb-catchtable@kb-catchtable: <sha>
[DEBUG] Copying plugin kb-catchtable@kb-catchtable to versioned cache (fallback to full copy) ← BUG #1
[DEBUG] Successfully cached plugin kb-catchtable@kb-catchtable at .../cache/kb-catchtable/kb-catchtable/<sha>
[DEBUG] Plugin kb-catchtable has no entry.skills defined ← BUG #1
[DEBUG] Total plugin skills loaded: 0 (for kb-catchtable)
After this sequence, cache/kb-catchtable/wip/0.1.0/ is gone as a side effect of the Removing old cached version step (BUG #2), even though wip was cleanly cached via path-based source moments earlier.
Bug 1 — fallback to full copy branch drops entry.skills
The cache path of the initial git clone is cache/<marketplace_name>/. The versioned cache target is cache/<marketplace_name>/<plugin_name>/<version>/. When marketplace_name == plugin_name, the versioned target's parent is the initial-clone target itself, which is already populated. Atomic rename fails, the code takes the Copying plugin ... to versioned cache (fallback to full copy) branch, and along that branch the entry.skills defaulting / skillsPath inference performed on the normal branch is missing:
- Normal branch (e.g.
adlc@claude-plugins-officialwhere marketplace name ≠ plugin name):Using versioned cached plugin ... → Loaded N skills. - Fallback branch (this case): produces a directory on disk but registers the plugin with
skillsPath=none, skillsPaths=0 pathseven whenplugin.jsondeclares"skills": ["./skills/"]explicitly.
Bug 2 — Removing old cached version wipes sibling plugins
When the url-based plugin is cached, Removing old cached version at .../cache/<marketplace_name> deletes the entire cache/<marketplace_name>/ directory before recreating it. In a multi-plugin marketplace, sibling plugins of that same marketplace are stored as cache/<marketplace_name>/<sibling>/<version>/ and get wiped collaterally — even when they were cached via a completely different source (path-based) and are not being refreshed.
Reproduction shows wip's cache/kb-catchtable/wip/0.1.0/ is destroyed, then never re-created in the same session, leaving wip in a broken state until the next refresh.
Workaround
Use path-based "./plugins/<name>" for the url-based plugin's source. This avoids both bugs at once: the normal-branch path is taken (no fallback), and no Removing old cached version step runs against the marketplace cache root.
Why these haven't surfaced widely
- "Plugin name == marketplace name" is the natural shape of single-plugin marketplaces (where the repo is the plugin). It is less common than multi-plugin marketplaces with
cache/<marketplace>/<plugin>/...paths that don't collide. - Multi-plugin marketplaces that also use url-based source for one of their plugins with this exact name collision are even rarer. So 100%-deterministic is masked by pattern rarity.
Suggested fix direction
- Bug 1: In the fallback-copy branch, run the same
entry.skillsdefaulting /skillsPathinference as the normal branch, so the two are observationally equivalent. - Bug 2: Scope
Removing old cached versionto the plugin's own versioned subdirectory (cache/<marketplace>/<plugin>/<version>/) instead ofcache/<marketplace>/, so unrelated sibling plugins are not affected. - (Defensive) Use a marketplace-independent versioned cache root (e.g.
cache/_versioned/<plugin>/<version>/) so neither cache path collisions nor sibling-plugin collateral damage are possible.
---
Issue authored with assistance from Claude. Reproduction was captured against a single Claude Code instance with cleared cache and no installed_plugins.json entry, ruling out multi-instance race conditions. Logs available on request. Maintainer verification welcome.This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗