[BUG] Claude Desktop: self-uploaded plugin cannot be updated or uninstalled — install record is frozen at first install

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

Component: Claude desktop app — plugins / "My Uploads" shelf
Severity: a self-published plugin cannot be updated or removed at all, and the
failure is silent until the next app restart.

Environment

| | |
|---|---|
| App | Claude desktop (CCD 2.1.266), MSIX Claude_1.52386.0.0_x64 |
| OS | Windows 11 Pro 10.0.26200 |
| Plugin | self-uploaded, id <plugin-id> |
| Marketplace | "My Uploads", <marketplace-id> |
| Installed | 2026-09-09T10:09:22.237679Z, at v1.2.0 (13 skills) |
| Observed | 2026-09-09 through 2026-09-11 |

Client plugin state referenced below lives at
%APPDATA%\Claude\local-agent-mode-sessions\<user-id>\<workspace-id>\rpm\.

Identifiers are redacted here. The affected plugin id, marketplace id and account
can be supplied privately on request — the install record in question is still in
its frozen state and available for inspection.

Summary

Uploading a new version of a self-published plugin persists correctly. Every
write after the original install does not
— install, update, uninstall. The
plugin view updates optimistically and reverts on restart, so a failed
server-side write is indistinguishable from success for the rest of the session.

There are two separable defects. They should be triaged separately, because
fixing either one alone changes the outcome.

Steps to reproduce

  1. Install a self-uploaded plugin from My Uploads (here: v1.2.0, 2026-09-09).
  2. Build v1.5.5 and upload it to My Uploads. **The marketplace entry correctly

shows 1.5.5.**

  1. Restart the app. The installed plugin still shows 1.2.0.
  2. Uninstall the plugin, reinstall from My Uploads, restart. Still 1.2.0.
  3. Uninstall it, then install a renamed build — byte-identical payload,

different name in plugin.json. Restart.

  1. Upload v1.9.1 to My Uploads. The upload persists; the marketplace entry

updates. The installed record stays at v1.2.0.

Expected vs actual

| After step | Expected | Actual |
|---|---|---|
| 3 | 1.5.5, 9 skills | 1.2.0, 13 skills |
| 4 | 1.5.5, 9 skills | 1.2.0, 13 skills |
| 5 | renamed plugin installed | never appears anywhere on disk or in the UI |
| 6 | marketplace 1.9.1, installed 1.9.1 | marketplace 1.9.1, installed 1.2.0 |

Step 5 is the informative one: a plugin with no prior installed record also
fails to install, so this is not version pinning or a stale cache of an existing
entry. The install path itself is not persisting.

Steps 2 and 6 show both sides cleanly: two separate uploads both persisted to the
marketplace, and neither reached the installed record.

Defect 1 — post-install mutations do not persist

[PluginsFetcher] fetchAccountEnabledPlugins returned 7 plugins before the
original install and 8 plugins from 2026-09-09 17:09:28 onward. Across every
sync on 2026-09-10 and 2026-09-11 it never returned to 7, through two
uninstall cycles.

So the original install did persist — the count moved 7 → 8 and stayed. What
never persists is every mutation after it. The UI reports success regardless; no
error is surfaced when the server-side write is rejected or dropped.

On-disk corroboration, unchanged through every attempt above:

  • <plugin-id>\.claude-plugin\plugin.json → "version": "1.2.0", mtime frozen

at 2026-09-09 17:09:30

  • <plugin-id>\skills\ → 13 directories, including four removed in later versions
  • rpm\manifest.json → updatedAt: 2026-09-09T10:09:22.237679Z,

updatedAtVerified: true

  • no directory for the renamed build exists anywhere under %APPDATA%\Claude

manifest.json is a live mirror, not a dead cache — it was observed being
rewritten eight seconds after a prior read, and the rewrite still returned the
2026-09-09 record. The client re-syncs, asks the account what is installed, and
the account keeps answering with the old state.

Defect 2 — re-upload does not rebind the install record

[RemotePluginManager] refreshes a user-installed plugin only when the remote
install record is newer than the local one. On 2026-09-11 08:24:51:

[RemotePluginManager] Refreshed 7 stale user-installed plugin(s)
  (remote_newer, remote_newer, remote_newer, remote_newer,
   remote_newer, remote_newer, remote_newer)

Seven of eight. The eighth is the self-uploaded plugin. In manifest.json the
seven first-party records carry updatedAt: 2026-09-10T20:23:0x; the
self-uploaded one still carries 2026-09-09T10:09:22.237679Z — the moment it was
first installed. Publishing a new version to the uploads shelf does not bump
the install record's updatedAt,
so remote_newer is never true and the client
never re-downloads.

Controlled experiment: forcing the gate open

To separate "the client never asks" from "the server answers with the old
version", the gate was forced by hand.

Method. In rpm\manifest.json, set this plugin's updatedAt to
2020-01-01T00:00:00.000000Z, leaving every other field and record untouched.
Restart the app.

Result. The gate fired, on exactly one plugin:

09:25:22 [RemotePluginManager] Refreshed 1 stale user-installed plugin(s) (remote_newer)
09:25:24 [RemotePluginManager] Sync complete: 1 downloaded, 0 removed, 0 orphans cleaned

The payload directory was re-materialised at 09:25:22 — its first write since
2026-09-09 17:09:30. What arrived was v1.2.0 with 13 skills, the same artifact
as before, and manifest.json was rewritten with updatedAt reset to the
server's value.

Conclusion. The client is not the blocker. When it does ask, the account
serves the artifact from the original install. Both the install record's
timestamp and its artifact binding are frozen at install time. Forcing a
refresh cannot deliver an update; it only re-downloads the original.

Ruled out

  • Not packaging. The v1.5.5 archive was verified three times by two

independent toolchains: 35 entries, manifest present, 9 skills, zero backslash
path separators, ASCII-only manifest, matching sha256.

  • Not the manifest version field. version was bumped every attempt, and the

marketplace entry reflects it correctly each time.

  • Not a name collision. The renamed build fails identically.
  • Not a stale local directory. The durable state is server-side, and

updatedAtVerified: true marks the local copy a verified cache. Replacing the
payload by hand does load the new skills — and survives both an app restart and
routine periodic syncs, because the local updatedAt then matches the server's
exactly so remote_newer cannot fire — but it never touches the install
record, and a forced refresh reverts it. A local workaround, not a
counter-example.

Impact

  1. A self-published plugin cannot be updated or removed through any supported UI

path.

  1. The stale version here serves four skills that also exist as account-level

Cowork skills, so two skills compete for each trigger. Those four were
deliberately removed in the newer version precisely to end that collision, and
the fix cannot be delivered.

  1. The silent failure is what makes it expensive: the user sees a successful

install or uninstall until the next restart, with no error anywhere.

Suggested fixes

  1. Surface the write failure. Do not update plugin state optimistically

without reconciling against the server response; show an error when the
install/uninstall write is rejected or dropped.

  1. Make post-install mutations persist — restoring uninstall/reinstall as a

manual upgrade route.

  1. Rebind existing installs on publish. Publishing a new version should point

existing install records at the new artifact and bump updatedAt so the
refresh gate fires.

Fix 2 alone restores a manual upgrade path. Fix 3 is what makes publishing an
update mean anything.

Related

  • #92234 — a broken plugin surviving uninstall/reinstall on Windows. I posted the log evidence above there as possibly related; a later report on that thread (the same plugin id failing across accounts, with the hook file present on disk but unreadable) points more toward the Sep 8 Windows drive-access incident than toward this frozen-install-record defect.
  • #85218 — uninstall not persisting for account-scoped plugins (closed as not planned).
  • #73907 — personal marketplace never re-syncing on Windows/Cowork (closed as not planned).

View original on GitHub ↗