[BUG] Plugin auto-update is silent in both directions: no diagnostic when no marketplace is eligible, no durable signal when an update lands

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 0 comments · opened Aug 6, 2026

Environment

  • Claude Code 2.1.222, native install (installMethod: "native"), macOS 15 (darwin 25.5.0)
  • Third-party (org-internal) marketplace added via extraKnownMarketplaces, GitHub source

Summary

Two related observability gaps in plugin auto-update. Both were encountered on the same investigation, which took several hours and was ultimately only resolvable by decompiling the bundle, because nothing is emitted at any log level.

  1. When no marketplace is eligible for auto-update, the pass returns with no diagnostic whatsoever -- not even at --debug.
  2. When an update does land, the only signal is a 10-second low-priority toast, and the running session silently continues using the old plugin version until /reload-plugins.

Part 1: silent no-op when nothing is eligible

mcm() (the plugin autoupdate pass) begins:

if (Aqe()) { C("Plugin autoupdate: skipped (auto-updater disabled)"); return }
let r = await xiT();
if (r.size === 0) return;                     // <-- no log, at any level
...
C("Plugin autoupdate: checking installed plugins");

The disabled case logs. The empty-eligible-set case does not. Eligibility comes from:

async function xiT(){
  let e = await my(), t = mne(), r = new Set;
  for (let [n,o] of Object.entries(e)) {
    if (!b0(o.source)) continue;
    if (Sze(n, o, t[n]?.autoUpdate)) r.add(n.toLowerCase())
  }
  return r
}
function Sze(e,t,r){ if (r !== void 0) return r;
                     return t.autoUpdate ?? (SZn.has(e.toLowerCase()) && !jpg.has(e.toLowerCase())) }
SZn = new Set(["claude-plugins-official","anthropic-marketplace","agent-skills", ...])   // first-party only

SZn is a first-party allowlist, so every third-party marketplace defaults to no auto-update. That is a defensible default. The problem is that opting in incorrectly is indistinguishable from opting in correctly: both produce total silence.

Why this is easy to hit

autoUpdate: true declared in a project's .claude/settings.json is not honored -- tB_() excludes projectSettings and localSettings from extraKnownMarketplaces (see the companion issue on trust-predicate asymmetry). So the natural thing to do, committing the marketplace declaration to a shared repo so a team picks it up, produces:

  • a config file that reads as correct,
  • a marketplace that is never fetched,
  • plugins pinned at whatever version was first installed, indefinitely,
  • and zero output anywhere, at any verbosity.

In our case a marketplace went 6 days without a fetch while autoUpdate: true sat in the repo config. The only externally visible symptom was known_marketplaces.json having a stale lastUpdated, which requires knowing the file exists.

Requested change

Emit a debug line when the eligible set is empty, naming the registered marketplaces that were skipped and the reason:

Plugin autoupdate: 0 of 2 marketplaces eligible (skipped: my-org-plugins -- autoUpdate not set and not first-party)

Ideally also surface effective auto-update state in claude plugin marketplace list --json, which currently returns only name, source, repo, installLocation -- there is no supported way to ask "will this marketplace auto-update?"

Part 2: an update that lands leaves no durable trace

When an update succeeds, notification is:

_$l({ key: "plugin-autoupdate-restart",
      segments: [{ text: `Plugins updated: ${VMO}`, color: "success" },
                 { text: " . Run /reload-plugins to apply", dim: true }],
      priority: "low", timeoutMs: 1e4 })
  • transient, 10 seconds, priority: "low"
  • suppressed entirely when sa() is true (qa() || ku() !== null, i.e. remote workspace / remote session)
  • the session keeps running the previously loaded plugin version until /reload-plugins is run manually

So a long-lived session can auto-update on disk and continue serving the old skills for hours, with the only indication having expired after ten seconds.

Observed concretely: in a session where the install record demonstrably moved 0.7.0 -> 0.8.0 at a known timestamp (verified against installed_plugins.json, version/installPath/gitCommitSha all changed), no notification was seen. We cannot say whether it was suppressed, generated but missed inside its 10s window, or never generated -- and that is precisely the complaint: after the fact there is no way to determine which. Nothing is logged about the notification decision, and no persistent state records that the running session is stale.

Requested change

Any one of these would resolve it:

  • log the notification decision (Plugin autoupdate notification: shown / suppressed (<reason>)) so it is diagnosable after the fact;
  • keep a persistent indicator while a session is running plugin versions older than what is installed on disk, rather than a 10s toast;
  • expose staleness in /status or claude plugin list (e.g. mark entries whose on-disk version differs from what the session loaded);
  • or reload eligible plugins automatically when it is safe to do so.

Reproduction (Part 1)

  1. Add a third-party marketplace: claude plugin marketplace add <org>/<repo>
  2. Install a plugin from it.
  3. Do not set autoUpdate anywhere (or set it only in the project's .claude/settings.json).
  4. Publish a new version of the plugin to that marketplace.
  5. Start a session, send a message, wait >10 minutes (past the random 0..600000ms sleep in mcm()).
  6. Observe: plugin stays at the old version. --debug output contains no line mentioning autoupdate, marketplaces, or eligibility.

Expected: some indication that auto-update ran and elected to do nothing, and why.

View original on GitHub ↗