Three intertwined bugs in the Remote/Worktree feature: MCP push, gh ENOENT, plugin-setup hang

Resolved 💬 4 comments Opened May 5, 2026 by McSlaine Closed Jun 4, 2026

Claude Code Desktop: Three intertwined bugs in the Remote/Worktree session feature

A single investigation session against a Linux remote (worktree mode) surfaced three distinct Claude Code Desktop bugs that each independently produce the user-facing error Something went wrong → Not connected to remote server. Fixing one reveals the next. Filing as one report because the misleading error label makes them look like the same bug to users.

Affected versions

  • Claude Code Desktop 1.5354.0 (windows-store), Electron 41.3.0, Node 24.15
  • Remote ccd-cli: 2.1.121 (binary at /root/.claude/remote/ccd-cli/)
  • Host OS: Windows 11 (10.0.22631), x64
  • Remote OS: Ubuntu 24.04, Tailscale-routed SSH

---

Bug 1: Local MCP extensions are pushed to Linux remotes with un-translated Windows paths

Summary

When a worktree session starts, the desktop client pushes the full local MCP extension list to the remote and tries to spawn each one there. The ${__dirname} token in each extension's mcp_config is expanded against the local Windows install path before the spawn payload is sent over the bridge, so the remote receives commands like:

node "C:\Users\<user>\AppData\Roaming\Claude\Claude Extensions\<ext-id>\dist\index.js" --stdio
uv  --directory "C:\Users\<user>\AppData\Roaming\Claude\Claude Extensions\<ext-id>" run <name>

These obviously fail on Linux. When all the spawned MCPs fail, Claude Code surfaces this to the user as the misleading Not connected to remote server — even though the remote daemon is healthy and pinging at 7s intervals.

Reproduction

  1. On a Windows machine running Claude Code Desktop, install three extensions from the registry:
  • ant.dir.cursortouch.windows-mcp (manifest declares platforms: ["win32"] only)
  • ant.dir.gh.wonderwhy-er.desktopcommandermcp (cross-platform manifest)
  • ant.dir.gh.anthropic.pdf-server-mcp (cross-platform manifest)
  1. Configure a remote SSH host (any Linux box) and start a Code session in worktree mode.
  2. Send any message — the chip shows the repo + worktree (so file ops are working) but the message returns Something went wrong → Not connected to remote server.

Evidence

/root/.claude/remote/remote-server.log shows the connection itself is fine — server.ping succeeds every 7s, git.worktree_create and files.read/list/stat all return hasError=false. The failure is exclusively in process.spawn. Three back-to-back at session start:

[process.Manager] Process … started, PID=…, command=uv
[process.Manager] Process … exited with code 2
[process.Manager] Process … started, PID=…, command=node
[process.Manager] Process … exited with code 1
[process.Manager] Process … started, PID=…, command=node
[process.Manager] Process … exited with code 1

Stderr from each spawn is streamed to the client, not the server log, so it doesn't appear there. A wrapper script on the remote captured actual argv and stderr:

uv ARGS: <--directory> <C:\Users\<user>\…\ant.dir.cursortouch.windows-mcp> <run> <windows-mcp>
uv stderr: error: No such file or directory (os error 2)

node ARGS: <C:\Users\<user>\…\ant.dir.gh.anthropic.pdf-server-mcp/dist/index.js> <--stdio>
node stderr: Error: Cannot find module '/root/C:\Users\<user>\…\dist/index.js' (MODULE_NOT_FOUND)

node ARGS: <C:\Users\<user>\…\ant.dir.gh.wonderwhy-er.desktopcommandermcp/dist/index.js>
node stderr: Error: Cannot find module '/root/C:\Users\<user>\…\dist/index.js' (MODULE_NOT_FOUND)

Root causes (each independently fixable)

  1. Platform-gated extensions are still pushed. windows-mcp declares compatibility.platforms: ["win32"]. The desktop should not attempt to spawn it inside a Linux remote session. Either filter by platforms against the remote's OS, or never push platform-gated extensions to mismatched targets.
  2. ${__dirname} is expanded too early. For both Win-only and cross-platform extensions, ${__dirname} is being resolved to the Windows install path before the spawn payload is sent over the bridge. On the remote, that path is meaningless. Either send the unexpanded token and let the remote resolve against its own extension cache, or upload the extension bundle (the way plugins are uploaded into /root/.claude/remote/plugins/) and resolve against that.
  3. Error surfacing is misleading. When every MCP process.spawn fails, the user sees Not connected to remote server. The remote IS connected. The actual failure (per-extension stderr, exit codes) should be surfaced or summarised in the Details panel.
  4. The "Local only" toggle in the Extensions UI doesn't appear to persist. A search through every JSON file under %APPDATA%\Claude\ and %USERPROFILE%\.claude* turned up no per-extension scope, local-only, allowRemote, or equivalent field. If that toggle is supposed to scope an extension and prevent push-to-remote, it isn't writing anywhere findable.

Workaround in use

Three small "no-op" stub MCP servers installed on the remote that respond to initialize/tools/list with empty capabilities and exit cleanly. The desktop's process.spawn then succeeds, session connects, the three Windows-resident MCPs are simply unavailable on the remote (which is the intended behaviour). Implementation details available on request.

---

Bug 2: gh CLI spawn fails with ENOENT even when the binary exists and is executable

Summary

After fixing Bug 1, sessions started but the worker crashed (Claude Code process exited with code 1) on the first user message. Sentry breadcrumb:

[error] Child process errored with 'spawn gh ENOENT'

Cause: a Claude Code plugin (likely feature-dev or superpowers from claude-plugins-official) calls the GitHub CLI on session start. gh was not installed on Windows, the spawn returned ENOENT, the plugin didn't catch it, the worker threw, and the entire Claude Code process exited 1. The desktop UI surfaced the local crash as Claude Code crashed → Claude Code process exited with code 1.

Sub-bug: spawn still fails after gh is installed

After installing gh via winget install --id GitHub.cli and restarting Claude Code, the Sentry breadcrumb changed to:

[error] Child process errored with 'spawn <drv>:\Program Files\GitHub CLI\gh.exe ENOENT'

(<drv>: is Sentry redaction; real path is C:\Program Files\GitHub CLI\gh.exe.)

Verified separately:

  • File exists, 38.7 MB, valid PE binary
  • BUILTIN\Users has ReadAndExecute, Synchronize
  • Defender real-time off; no Defender detections of gh.exe
  • A direct [System.Diagnostics.Process]::Start(…gh.exe, "--version") returns exit 0 and prints gh version 2.92.0

So the binary is fully spawnable from the same user/session — but Claude Code's worker still gets ENOENT. Possible angles:

  • The plugin is invoking via a shape Node's child_process.spawn rejects (cwd doesn't exist, weird stdio config, etc.). Note that on Windows, libuv spawn returns ENOENT for either a missing executable OR a missing cwd — the error is ambiguous.
  • The plugin uses a stale/cached PATH from before gh was installed despite a full app restart.
  • An interaction with electron's process sandbox.

Root causes

  1. An unhandled ENOENT from a plugin's child_process tears down the entire worker. Plugin spawn failures should be isolated, not fatal to the session.
  2. The post-install spawn-ENOENT case — needs investigation by someone with access to the plugin source. The error label is misleading because the binary IS present and executable.
  3. The user-facing label is wrong. "Claude Code crashed → exited with code 1" is correct as a fact, but the underlying cause (a plugin's GitHub-CLI call) is opaque and the user is given no path to diagnose.

---

Bug 3: New session hangs indefinitely on "setting up plugins" with no error

Summary

After disabling feature-dev and superpowers (the two plugins suspected of calling gh), the next session attempt no longer crashed but hung indefinitely on the UI label "setting up plugins". No error was raised, no Sentry breadcrumb at error level, no crash.

Evidence

Both sides were idle and healthy:

  • Server: MCP startup health-check completed at clean exit code 0 for all three stubs at 15:17:33; afterwards only server.ping every 7s, all hasError=false. No file writes, no spawns, no errors.
  • Desktop: routine HTTP traffic only — heartbeats every 20s, repeated plugins/list-plugins and marketplaces/list-account-marketplaces fetches, OAuth token refresh, polling. No errors, no warnings of substance, no crashes.

Root cause (suspected)

The desktop client appears to wait for a signal that the server is never going to send. It's a UI state-machine bug — there's no underlying failure to recover from, just an unblocked wait. Speculation: maybe an enabledPlugins reconciliation step waits for a per-plugin "ready" message that the disabled plugins never deliver?

Why this matters

The same UX label ("setting up plugins") combined with a hang produces the same user experience as a hard failure, with no path to diagnose. From the user's perspective this is just "broken with no error".

---

Suggested priority

Medium-high. Bug 1 alone fully blocks the remote/worktree feature for any user who has a Windows-only or path-tokenised MCP installed (windows-mcp is popular). Bug 2 makes any plugin invoking gh a session-killer for users without gh installed (and apparently for some users with gh installed). Bug 3 stalls sessions silently.

The error labels share a single misleading pattern: failures inside the desktop's spawn/setup pipeline get reported as Not connected to remote server or as a generic crash, even when the remote bridge is healthy and pinging. Two-three hours of investigation here landed on each underlying cause one at a time. Better diagnostics in the Details panel — actual stderr from failed spawns, named which plugin called the failed binary, etc. — would have shortened that to minutes.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗