[BUG] Claude Desktop - Plugin uninstall does not persist: account-scoped marketplace plugins are silently re-downloaded on next launch, and server-disabled plugins are never removed from disk
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Environment: Claude Desktop 1.26832.0, macOS (Darwin 25.4.0). Plugin: audit-project v3.7.2, account-scoped marketplace awesome-claude-plugins, installedBy: "user", installationPreference: "available".
Uninstalling a plugin through the desktop UI removes it locally but does not durably record the user's intent. The plugin is fully restored on the next application launch. On the flip side, when deleting fro claude.ai, and the backend does reflect the disable, the plugins shows as disabled, but the local files are never cleaned up - so there is no state in which an uninstalled plugin is actually gone.
Defect 1 - Uninstall writes no tombstone for account-scoped plugins
The uninstall path mutates only local state: it rewrites manifest.json (removing the entry entirely, not tombstoning it) and hard-deletes the plugin directory. No ~/.Trash entry, no staging copy.
The codebase has a purpose-built mechanism to prevent resurrection - a tombstone marker consumed by the sync path via honorTombstone: i !== 'required'. But the tombstone writer is gated on plugin origin:
t === org-provisioned && await o(uninstall_tombstone, ..., () => y.o(e, n.orgUninstalledFile))
Account-scoped marketplace plugins are not org-provisioned, so no tombstone is ever written. The checker looks for a marker the writer never produces for this install path, leaving no durable record that the user removed the plugin.
Defect 2 - Startup reconcile restores the plugin
On launch, RemotePluginManager reconciles local plugins against the backend's account-enabled set. Because the enabled flag was never cleared, the server still reports the plugin as enabled, the reconciler computes a delta, and re-downloads it. Observed across three consecutive restarts:
11:47:05 fetchAccountEnabledPlugins: 3 plugins → Downloaded 2
11:53:38 [user uninstall — nothing logged]
12:00:16 fetchAccountEnabledPlugins: 3 plugins → Downloaded 1 ← restored
~12:02 [user uninstall #2 — nothing logged]
12:03:08 fetchAccountEnabledPlugins: 3 plugins → Downloaded 1 ← restored
Defect 3 - Reconcile is one-directional; disabled plugins are never reaped
RemotePluginManager downloads what is missing but never removes what the backend no longer authorizes. Across every sync observed in a full session, the removal counters were always zero:
Sync complete: N downloaded, 0 removed, 0 orphans cleaned
This held even when the two states disagreed in the removal direction. At 12:10:36 the backend dropped to fetchAccountEnabledPlugins: 2 plugins while three plugins remained on disk.
Combined effect: while backend state lags, uninstall is cosmetic and reverts on restart; once backend state catches up, cleanup never happens. The plugin's code persists on disk either way.
Impact
- A user who uninstalls a plugin because they do not trust it, has that plugin's code restored to ~/Library/Application Support/ unattended at next launch.
- No available user action makes the uninstall stick: the local button does not persist, and after backend disable the files are still not removed.
What Should Happen?
Uninstalling a plugin through the UI should be durable and complete:
- The removal persists across restarts. Uninstall should either clear the account-enabled flag before reporting success, or write a local tombstone that the startup reconcile honors - the same protection auto_install plugins already get via honorTombstone. A plugin the user removed should not return without an explicit user action.
- Backend-disabled plugins are reaped locally. When the account-enabled set no longer contains a plugin, the reconcile should delete its directory and manifest entry, so Sync complete: … N removed reflects real cleanup rather than always reporting 0 removed.
- If a removed plugin is intentionally restored by server policy, the user should be informed.
Error Messages/Logs
Steps to Reproduce
- Uninstall a plugin via the desktop UI (Settings → Customize → plugins → plugin → Uninstall).
- Confirm the local removal. The manifest entry is spliced out and the directory is hard-deleted.
- Quit and relaunch Claude Desktop.
- Observe the resurrection.
ls $RPM # plugin_<id>/ is back
grep '"name"' $RPM/manifest.json # entry restored
- Correlate against the startup sync:
grep -E "fetchAccountEnabledPlugins|RemotePluginManager" ~/Library/Logs/Claude/main.log | tail -20
- → Observed: fetchAccountEnabledPlugins: 3 plugins (unchanged from before the uninstall) → Downloaded 1 account-enabled plugin(s) → Sync complete: 1 downloaded, 0 removed (Defects 1 & 2).
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude Desktop 1.26832.0
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
_No response_