[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

Status Open
Reported on v2.1.272
Maintainer reply None cached
Activity 0 comments · opened Sep 15, 2026

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-a fails on the dead path (Failed to refresh marketplace 'mkt-a': ENOENT … '/nonexistent/mkt-a') instead of resolving from source.
  • claude plugin list shows ✘ 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 remove then add, does write a correct installLocation. But remove also deletes the marketplace's plugins from enabledPlugins. 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?

  1. When a directory source is re-fetched because the recorded installLocation is missing, write the resolved source.path back as installLocation.
  2. claude plugin marketplace update should resolve a directory marketplace from source, not from the recorded location.
  3. remove + add should not be the only recovery, given that remove drops the marketplace's plugins from enabledPlugins (#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: installLocation is 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 installLocation formats.
  • #94451: the sibling registry bug, where an invalid known_marketplaces.json is never repaired.

View original on GitHub ↗