[BUG] Plugin auto-update is silent in both directions: no diagnostic when no marketplace is eligible, no durable signal when an update lands
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.
- When no marketplace is eligible for auto-update, the pass returns with no diagnostic whatsoever -- not even at
--debug. - 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-pluginsis 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
/statusorclaude 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)
- Add a third-party marketplace:
claude plugin marketplace add <org>/<repo> - Install a plugin from it.
- Do not set
autoUpdateanywhere (or set it only in the project's.claude/settings.json). - Publish a new version of the plugin to that marketplace.
- Start a session, send a message, wait >10 minutes (past the random
0..600000mssleep inmcm()). - Observe: plugin stays at the old version.
--debugoutput contains no line mentioning autoupdate, marketplaces, or eligibility.
Expected: some indication that auto-update ran and elected to do nothing, and why.