[BUG] Desktop-provisioned plugins load as `@inline` but `plugin disable` targets `@<marketplace>` — disable is unreachable, CLI falsely reports "already disabled"

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

Summary

A plugin provisioned by the desktop app is loaded into every Claude Code
session as <name>@inline, but claude plugin disable and enabledPlugins
resolve the same plugin as <name>@<marketplace>. The two identities never
match, so the plugin can never be disabled from the CLI — while the CLI
confidently reports it as already disabled.

Net effect: an MCP server the user has never invoked (usageCount: 0 across 97
startups) is spawned once per session and stays resident for the lifetime of the
desktop app, with no supported way to turn it off.

This is the same manifest-load-vs-enable-check seam reported in #58806 (closed
COMPLETED 2026-05-17). That issue is locked, so per the lock bot's instruction I
am filing a new one. The entry point differs: #58806 was a marketplace plugin
explicitly set to false; here the plugin is injected by the desktop app under a
different namespace, so the enable check is not merely bypassed — it is
unaddressable.

Environment

  • macOS (Darwin 27.0.0), Apple Silicon
  • Claude Desktop 2.110.1
  • Claude Code 2.1.271
  • Plugin: browser-use, marketplace knowledge-work-plugins,

installedBy: "user", installationPreference: "available"

Evidence

1. The CLI does not manage the plugin, yet claims it is disabled

claude plugin list shows 22 plugins; browser-use is not among them.

$ claude plugin disable browser-use --json
{"outcome":"failed","message":"Plugin \"browser-use\" not found in any editable
 settings scope. Use plugin@marketplace format.","failureCode":"ambiguous_marketplace"}

$ claude plugin disable browser-use@knowledge-work-plugins --json
{"outcome":"failed","message":"Plugin \"browser-use@knowledge-work-plugins\" is
 already disabled","failureCode":"already_in_goal_state","alreadyInGoalState":true}

Meanwhile ~/.claude.json records the loaded plugin under a different identity:

"pluginUsage": {
  "browser-use@inline": { "usageCount": 0, "lastUsedNumStartups": 97 }
}

@inline vs @knowledge-work-plugins — the disable command targets a namespace
the loader never consults.

2. Explicit false does not prevent the spawn

Wrote to ~/.claude/settings.json at 21:56:53:

"enabledPlugins": { "browser-use@knowledge-work-plugins": false }

A session process started at 21:56:58 (5s later) still spawned the server:

PID 45705  claude ... --setting-sources=user,project,local   (session, started 21:56:58)
 └─ PID 45749  uv tool uvx --python 3.12 browser-use@latest --cli-mcp   (21:56:58)
     └─ PID 45866  python .../bin/browser-use --cli-mcp                 (21:57:04)

3. The app does not pass plugin state to the CLI

The --settings payload on the session's command line contains only
ultracode and deniedMcpServers — no plugin keys. The string browser-use
appears 0 times in the 3378-byte command line. The CLI resolves the plugin
itself, from the app-provisioned RPM plugin directory:

// <app-support>/local-agent-mode-sessions/<...>/rpm/<plugin-id>/.mcp.json
{ "browser-use": { "command": "uvx",
  "args": ["--python","3.12","browser-use@latest","--cli-mcp"] } }

Note manifest.json in that directory is rewritten by the app after launch
(observed mtime 20h after app start), so editing it is not a durable workaround.

4. Controlled comparison — the @inline key is the one that works

Same machine, same running desktop app, three sessions differing only in which
enabledPlugins key was present when they started:

| session started | keys set in enabledPlugins | MCP server spawned? |
|---|---|---|
| 21:56:21 | (none) | yes |
| 21:56:58 | browser-use@knowledge-work-plugins: false | yes |
| 22:00:15 | + browser-use@inline: false | no |

So the plugin is gated by enabledPlugins — but only under the @inline
identity. claude plugin disable never writes that key, and offers no way to
address it: passing the bare name is rejected as ambiguous_marketplace, and
passing @knowledge-work-plugins is accepted but targets a namespace the loader
does not consult, returning already_in_goal_state.

Workaround (undocumented)

// ~/.claude/settings.json
"enabledPlugins": { "browser-use@inline": false }

Verified effective on the next session start. Users cannot discover this: the
@inline identity appears only in ~/.claude.json's pluginUsage map, and no
CLI command writes or lists it.

Suggested fix

  1. Have claude plugin disable <name> resolve the identity the loader actually

uses, or disambiguate across all namespaces including @inline, rather
than only marketplace ones.

  1. Do not return already_in_goal_state for a plugin that is currently loaded —

that answer is false whenever the two namespaces disagree.

  1. Surface app-provisioned plugins in claude plugin list; today they are

invisible to it while being active in every session.

Reproduction

  1. Have a desktop-app-provisioned plugin that ships an .mcp.json stdio server.
  2. claude plugin disable <name>@<marketplace> → reports already disabled.
  3. Set enabledPlugins["<name>@<marketplace>"] = false in ~/.claude/settings.json.
  4. Open a new session.
  5. Expected: no MCP server spawns.

Actual: the server spawns; ps shows a uv/python child per session.

Impact

  • Per session: one uv wrapper + one Python interpreter, ~145 MB RSS combined,

for a server with usageCount: 0.

  • Because session processes are retained for the app's lifetime (see #76268,

#88524), this multiplies: on this machine, 10 sessions opened over ~20h held
11 such trees, ~1.6 GB RSS, at ~0% CPU.

  • No discoverable off switch. A workaround exists (below) but is undocumented

and unreachable through any CLI command. The CLI's answer ("already disabled")
is actively misleading — a user following the documented path is told the
problem is solved while nothing changes.

Notes

  • The stdio-MCP spawn-to-discover-tools requirement makes some eager start

unavoidable; the defect is the unreachable disable, not the spawn itself.

  • Tool schemas for this server were already deferred (ToolSearch-gated) in the

session, so deferral exists at the schema layer but not at the process layer.

View original on GitHub ↗