Marketplace plugins cached without .claude-plugin/plugin.json load inline under version-string names, duplicating and merging plugin namespaces
Environment
- Claude Code Desktop on macOS (sessions that load plugins inline;
pluginUsagein~/.claude.jsonrecords@inlineentries) - Plugins installed from marketplaces via
claude plugin install
What happens
When a marketplace defines a plugin purely through its marketplace.json entry (no .claude-plugin/plugin.json in the plugin source), the installer materializes the cache at ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ without writing a plugin manifest.
Sessions that load plugins inline then derive the plugin name from the cache directory basename — the version string — instead of the marketplace registration name. Consequences observed:
- Every component appears twice.
complete@policyengine-claudev3.23.0 exposes bothcomplete:country-models:ci-fixerand3.23.0:country-models:ci-fixer(and likewise for every agent, command, and skill in the plugin). - Distinct manifest-less plugins with the same version string merge into one namespace:
plugin-dev@claude-code-pluginsv0.1.0 andcosilico@cosilicov0.1.0 both loaded as0.1.0:*, mixing their components in the agent/skill lists. ~/.claude.jsonpluginUsageaccumulates keys like"3.23.0@inline"(18k+ uses on this machine) alongside the correctly-named"complete@policyengine-claude".
A plugin whose manifest sits at the plugin root (plugin.json, old layout — cosilico@cosilico) is affected too: only .claude-plugin/plugin.json appears to be consulted. Plugins with a proper .claude-plugin/plugin.json collide on the same name as their marketplace registration and dedupe cleanly (e.g. rules-foundation).
Repro
claude plugin marketplace add PolicyEngine/policyengine-claudeclaude plugin install complete@policyengine-claude— this plugin is defined only by amarketplace.jsonentry, so its cache dir contains no.claude-plugin/plugin.json- Open a Claude Code Desktop session → the available agents/skills list shows both
complete:*and3.23.0:*with identical contents
Expected
Either:
- (a) the installer synthesizes
.claude-plugin/plugin.jsonfrom the marketplace entry when materializing the cache (name, version, and description are all present in the entry), or - (b) the inline loader resolves the plugin name from the marketplace registration rather than falling back to the directory basename.
Workaround
A SessionStart hook that synthesizes the missing manifests (re-runs each session because plugin updates regenerate cache dirs):
#!/bin/bash
CACHE="$HOME/.claude/plugins/cache"
[ -d "$CACHE" ] || exit 0
for version_dir in "$CACHE"/*/*/*/; do
[ -f "$version_dir/.claude-plugin/plugin.json" ] && continue
plugin=$(basename "$(dirname "$version_dir")")
version=$(basename "$version_dir")
mkdir -p "$version_dir/.claude-plugin"
if [ -f "$version_dir/plugin.json" ]; then
cp "$version_dir/plugin.json" "$version_dir/.claude-plugin/plugin.json"
else
printf '{"name": "%s", "version": "%s"}\n' "$plugin" "$version" \
> "$version_dir/.claude-plugin/plugin.json"
fi
done
🤖 Generated with Claude Code
_Claude Code version: 2.1.175 (Claude Code)_