plugin marketplace update: installLocation validation compares literal paths without resolving symlinks, breaking shared plugin stores across config dirs
Environment
- Claude Code 2.1.226, macOS (darwin 25.x)
Summary
claude plugin marketplace update <mp> validates the recorded installLocation of a git-source marketplace by literal string comparison against <current CLAUDE_CONFIG_DIR>/plugins/marketplaces — without resolving symlinks (realpath). In setups where multiple CLAUDE_CONFIG_DIR profiles share one physical plugins store via a symlink (<profile>/plugins → shared dir), the update succeeds only from the one profile whose literal path happens to be recorded, and fails from every other profile with:
✘ Failed to update marketplace(s): Failed to refresh marketplace 'repro-git': Marketplace 'repro-git' has a corrupted installLocation (/tmp/claude/uprepro/home-a/plugins/marketplaces/repro-git) — expected a path inside /tmp/claude/uprepro/home-b/plugins/marketplaces. This can happen after cross-platform path writes or manual edits to known_marketplaces.json. Run `claude plugin marketplace remove repro-git` and re-add it.
— even though the recorded path and the expected path resolve to the same physical directory.
In our production setup this state was created purely by claude plugin marketplace add (github source) run from one profile — no manual edits — and every other profile then permanently fails to update.
Minimal reproduction (verified on 2.1.226)
The script below synthesizes the post-add state of a github-source marketplace offline (a local clone + the same known_marketplaces.json entry shape that add writes), so it runs without network. With a real github-source marketplace the same state arises naturally from add.
R=/tmp/claude/uprepro; rm -rf "$R"; mkdir -p "$R/home-a" "$R/home-b"
# a tiny valid marketplace repo
cd /tmp/claude && git init -q uprepro-mp && cd uprepro-mp
mkdir -p .claude-plugin plugins/hello/.claude-plugin
printf '%s' '{"name": "repro-mp", "owner": {"name": "repro"}, "plugins": [{"name": "hello", "source": "./plugins/hello", "description": "repro plugin"}]}' > .claude-plugin/marketplace.json
printf '%s' '{"name": "hello", "version": "0.0.1", "description": "repro plugin"}' > plugins/hello/.claude-plugin/plugin.json
git add -A && git commit -qm init
# profile A: initialize plugins dir (any marketplace add will do), then a git-source entry
CLAUDE_CONFIG_DIR="$R/home-a" claude plugin marketplace add /tmp/claude/uprepro-mp
mkdir -p "$R/home-a/plugins/marketplaces"
git clone -q /tmp/claude/uprepro-mp "$R/home-a/plugins/marketplaces/repro-git"
jq --arg loc "$R/home-a/plugins/marketplaces/repro-git" \
'. + {"repro-git": {"source": {"source": "github", "repo": "example/repro-mp"}, "installLocation": $loc, "lastUpdated": "2026-08-09T00:00:00.000Z", "autoUpdate": false}}' \
"$R/home-a/plugins/known_marketplaces.json" > "$R/km.tmp" && mv "$R/km.tmp" "$R/home-a/plugins/known_marketplaces.json"
# profile B shares the SAME physical plugins store via symlink
ln -sfn "$R/home-a/plugins" "$R/home-b/plugins"
# update from A (literal path matches): succeeds
CLAUDE_CONFIG_DIR="$R/home-a" claude plugin marketplace update repro-git # ✔ exit 0
# update from B (same physical store via symlink): fails
CLAUDE_CONFIG_DIR="$R/home-b" claude plugin marketplace update repro-git # ✘ exit 1, error above
# the two paths are the same physical directory
python3 -c "import os; a='$R/home-a/plugins/marketplaces/repro-git'; b='$R/home-b/plugins/marketplaces/repro-git'; print(os.path.realpath(a)==os.path.realpath(b))" # True
Observed on 2.1.226: A succeeds (exit 0), B fails (exit 1) with the error quoted above, realpath equality prints True.
Expected behavior
The installLocation validation should compare resolved paths — e.g. accept the entry when realpath(installLocation) lies inside realpath(<config>/plugins/marketplaces) — so that symlinked shared plugin stores (a common multi-profile arrangement) can update the marketplace from any profile.
Impact
- With N profiles sharing one plugins store,
marketplace updateworks from exactly one profile and permanently fails from the other N-1. - The suggested remediation in the error message (
remove+ re-add) recreates the clone and can invalidate other recorded paths that point into it.
Workaround
Rewrite installLocation to a path under a dedicated config home whose plugins is a symlink into the shared store, and run updates only from that config home (path-equivalent by symlink, verified working). This works but must be re-applied whenever the recorded path drifts.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗