[BUG] A directory marketplace whose recorded installLocation no longer exists never loads again: every launch re-fetches from source but keeps the dead path, and claude plugin marketplace update fails on it
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
Take a directory-source marketplace whose installLocation in known_marketplaces.json points at a path that no longer exists, while its source.path still exists. That marketplace's plugins never load again.
Every launch notices the problem: Cache corrupted or missing for marketplace mkt-a, re-fetching from source: ENOENT …. It bumps the entry's lastUpdated and leaves installLocation pointing at the dead path, so the next launch misses in exactly the same way. The file gets rewritten on every launch, just never the field that is wrong.
The CLI doesn't fix it either:
claude plugin marketplace update mkt-afails on the dead path (Failed to refresh marketplace 'mkt-a': ENOENT … '/nonexistent/mkt-a') instead of resolving fromsource.claude plugin listshows✘ failed to load — Marketplace mkt-a failed to load: cache-miss, and nothing tells the user which command recovers it.- The one recovery that works,
claude plugin marketplace removethenadd, does write a correctinstallLocation. Butremovealso deletes the marketplace's plugins fromenabledPlugins. After "recovering", the plugin still doesn't load, because it is now disabled.
For a directory source, Claude Code itself records installLocation equal to source.path when it adds the marketplace. So once the two diverge (for example, the file was carried across machines or config dirs; cf. #36575, #82272), the entry stays stuck, even though the correct value is sitting in the same entry.
What Should Happen?
- When a
directorysource is re-fetched because the recordedinstallLocationis missing, write the resolvedsource.pathback asinstallLocation. claude plugin marketplace updateshould resolve a directory marketplace fromsource, not from the recorded location.remove+addshould not be the only recovery, given thatremovedrops the marketplace's plugins fromenabledPlugins(#82272 notes the same side effect).
Error Messages/Logs
# every launch (debug log)
[DEBUG] Registered 1 hooks from 1 plugins
[WARN] Cache corrupted or missing for marketplace mkt-a, re-fetching from source: ENOENT: no such file or directory, open '/nonexistent/mkt-a'
$ claude plugin marketplace update mkt-a
Updating marketplace: mkt-a...✘ Failed to update marketplace(s): Failed to refresh marketplace 'mkt-a': ENOENT: no such file or directory, open '/nonexistent/mkt-a'
Steps to Reproduce
Runs against an isolated config dir. No login is needed, because plugin loading runs before the auth check (each -p launch exits with Not logged in, which does not affect the result).
# Isolated config dir; no login needed (plugin loading runs before the auth check).
R=$(mktemp -d); mkdir -p "$R/cfg/plugins" "$R/home"
for m in a b; do
d="$R/mkt-$m"; p="$d/plugins/plugin-$m"
mkdir -p "$d/.claude-plugin" "$p/.claude-plugin" "$p/hooks"
echo "{\"name\":\"mkt-$m\",\"owner\":{\"name\":\"demo\"},\"plugins\":[{\"name\":\"plugin-$m\",\"source\":\"./plugins/plugin-$m\"}]}" > "$d/.claude-plugin/marketplace.json"
echo "{\"name\":\"plugin-$m\",\"version\":\"0.0.1\"}" > "$p/.claude-plugin/plugin.json"
echo '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"true"}]}]}}' > "$p/hooks/hooks.json"
done
cat > "$R/cfg/settings.json" <<EOF
{"enabledPlugins":{"plugin-a@mkt-a":true,"plugin-b@mkt-b":true},
"extraKnownMarketplaces":{"mkt-a":{"source":{"source":"directory","path":"$R/mkt-a"}},
"mkt-b":{"source":{"source":"directory","path":"$R/mkt-b"}}}}
EOF
KM="$R/cfg/plugins/known_marketplaces.json"
cc() { HOME="$R/home" CLAUDE_CONFIG_DIR="$R/cfg" claude "$@"; }
echo "== mkt-a's directory source exists, but its recorded installLocation does not"
cat > "$KM" <<EOF
{"mkt-a":{"source":{"source":"directory","path":"$R/mkt-a"},"installLocation":"/nonexistent/mkt-a","lastUpdated":"2026-01-01T00:00:00.000Z"},
"mkt-b":{"source":{"source":"directory","path":"$R/mkt-b"},"installLocation":"$R/mkt-b","lastUpdated":"2026-01-01T00:00:00.000Z"}}
EOF
for i in 1 2; do
cc -p noop --debug-file "$R/launch$i.log" >/dev/null 2>&1
echo "-- launch $i"; grep -hE 'Registered|Cache corrupted' "$R/launch$i.log" | cut -c26-
grep -o '"installLocation": *"/nonexistent[^"]*"\|"lastUpdated": *"[^"]*"' "$KM" | head -1
done
cc plugin marketplace update mkt-a
cc plugin list
echo "== the CLI recovery: remove + add"
cc plugin marketplace remove mkt-a
grep -A3 enabledPlugins "$R/cfg/settings.json"
cc plugin marketplace add "$R/mkt-a"
cc -p noop --debug-file "$R/launch3.log" >/dev/null 2>&1
grep -h 'Registered' "$R/launch3.log" | cut -c26-
cc plugin list
Observed on 2.1.272 (verbatim; ANSI colour codes stripped):
== mkt-a's directory source exists, but its recorded installLocation does not
-- launch 1
[DEBUG] Registered 1 hooks from 1 plugins
[WARN] Cache corrupted or missing for marketplace mkt-a, re-fetching from source: ENOENT: no such file or directory, open '/nonexistent/mkt-a'
"installLocation": "/nonexistent/mkt-a"
-- launch 2
[DEBUG] Registered 1 hooks from 1 plugins
[WARN] Cache corrupted or missing for marketplace mkt-a, re-fetching from source: ENOENT: no such file or directory, open '/nonexistent/mkt-a'
"installLocation": "/nonexistent/mkt-a"
Updating marketplace: mkt-a...✘ Failed to update marketplace(s): Failed to refresh marketplace 'mkt-a': ENOENT: no such file or directory, open '/nonexistent/mkt-a'
Installed plugins:
❯ plugin-a@mkt-a
Version: unknown
Scope: user
Status: ✘ failed to load
Error: Marketplace mkt-a failed to load: cache-miss
❯ plugin-b@mkt-b
Version: 0.0.1
Scope: user
Status: ✔ enabled
== the CLI recovery: remove + add
✔ Successfully removed marketplace: mkt-a
"enabledPlugins": {
"plugin-b@mkt-b": true
},
"extraKnownMarketplaces": {
Adding marketplace…✔ Successfully added marketplace: mkt-a (declared in user settings)
[DEBUG] Registered 1 hooks from 1 plugins
Installed plugins:
❯ plugin-b@mkt-b
Version: 0.0.1
Scope: user
Status: ✔ enabled
Both launches load only plugin-b, and installLocation stays dead even though the entry is rewritten each time (its lastUpdated changes). After remove + add, plugin-a@mkt-a is gone from enabledPlugins and from claude plugin list, so the next launch still loads one plugin.
Claude Model
Not applicable. The failure happens before any model call.
Is this a regression?
I don't know. I haven't tested older versions.
Last Working Version
_No response_
Claude Code Version
2.1.272 (Claude Code)
Platform
Other. Reproduces without authentication.
Operating System
Ubuntu/Debian Linux (WSL2)
Terminal/Shell
bash, non-interactive (claude -p and claude plugin … subcommands)
Additional Information
Related, but not the same bug:
- #82272:
installLocationis validated by string prefix, so a symlinked config dir is flagged as corrupted. Here the path really does not exist, on a single config dir. - #36575 (closed): non-portable
installLocationformats. - #94451: the sibling registry bug, where an invalid
known_marketplaces.jsonis never repaired.