[BUG] Desktop GUI SSH: opening multiple Claude Code sessions on the same remote host invalidates each other's auth token, all sessions hang
Summary
When using the macOS Claude Code Desktop app's GUI SSH feature to drive Claude Code on a remote Linux host, opening more than one session against the same remote host causes all sessions to hang indefinitely. With a single session everything works; the second session is what triggers the failure.
The root cause on the remote side is that each new Desktop session spawns a brand-new ~/.claude/remote/server --serve with a brand-new auth token, invalidating the previous --serve's token. Older sessions' bridges are then rejected with Unauthorized request and disconnected.
Environment
| Component | Version |
|---|---|
| Local: macOS Claude Code Desktop | 1.4758.0 (fb266c) 2026-04-24 (latest) |
| Remote: ccd-cli (bundled & pushed by Desktop client) | 2.1.119 |
| Remote: ~/.claude/remote/server | claude-ssh e9237552c61a71aae8c0e586311078ab3275a0f2 (built 2026-04-22) |
| Remote OS | Ubuntu 24.04.4 LTS, x86_64, kernel 6.8 |
| Connection | macOS Desktop → GUI SSH → single remote Linux host |
Steps to reproduce
- Launch macOS Claude Code Desktop.
- Connect to a remote Linux host via GUI SSH; start a Claude Code session against a project on that host. Confirm replies come back normally. ✅
- While the first session is still open, open a second session in the Desktop GUI against the same remote host (any project).
- Send a message in either session.
Expected: Both sessions remain functional and independent, or the second session reuses the existing remote bridge.
Actual: Neither session returns. The chat UI hangs with no error.
Root cause (remote-side evidence)
1. Each new Desktop session spawns a new --serve with a new token
Within ~10 minutes of repeated GUI-SSH activity, multiple --serve generations stacked up, each with a different --token-file:
PID CMD
389774 .../server --serve --token-file token.<hashA> (10:52)
390185 .../server --serve --token-file token.<hashB> (10:53)
395604 .../server --serve --token-file token.<hashC> (10:58)
400203 .../server --serve --token-file token.<hashD> (11:02)
403026 .../server --serve --token-file token.<hashE> (11:06)
Multiple --bridge processes existed concurrently, but only the most recent one held a valid token.
2. Unauthorized request in ~/.claude/remote/remote-server.log
[Server] New connection from: @
[Server] RPC request: method=server.capabilities, id=1
[Server] RPC response sent: id=1, hasError=false ← bridge A authenticated, healthy
[Server] RPC request: method=server.ping, id=2
[Server] RPC response sent: id=2, hasError=false
[Server] New connection from: @ ← bridge B (a second session) connects
[Server] RPC request: method=server.ping, id=0 ← fresh client, RPC counter restarts at 0
[Server] Unauthorized request: method=server.ping, id=0 ← rejected: presents the previous --serve's token
[Server] Connection closed: @
Unauthorized can only be emitted when a bridge presents a token that the current --serve does not recognize — i.e. at least two clients started against two different --serve generations, which is exactly the multi-session scenario.
3. Downstream: SSH reconnect storm
Once older bridges are kicked, the Desktop client auto-reconnects, spawning yet another --serve, which invalidates the latest token again. journalctl -u ssh shows new Accepted publickey lines every 5–10 seconds for the same source until the user quits the Desktop app. This looks similar to #48530 but is a consequence of token churn here, not a single-session connection flood.
How I recovered (verified)
After hitting this hang, the following sequence reliably recovers the host:
- On the Mac:
Cmd+Qto fully quit Claude Code Desktop (closing windows alone is not enough — make sure no menu-bar icon is left and no remote-driving process is still alive). - On the remote host: kill the residual server processes and remove the stale socket / token files:
``bash``
pkill -f 'claude/remote/server'
rm -f ~/.claude/remote/rpc.sock ~/.claude/remote/token.*
- On the Mac: relaunch Claude Code Desktop and open only one session against the remote host. Send a message — it returns normally.
After step 3, remote-server.log shows a clean, steady heartbeat with no Unauthorized:
[Server] RPC request: method=server.ping, id=N → hasError=false
[Server] RPC request: method=server.ping, id=N+1 → hasError=false
...
ps shows exactly one --serve and one --bridge; journalctl -u ssh shows ~4 Accepted publickey lines per 2 minutes (normal keepalive), no reconnect storm. The single --serve instance keeps its token stable for the lifetime of the session.
Current confirmed working configuration
On the same versions listed above (Desktop 1.4758.0, ccd-cli 2.1.119, server build e923755…), the GUI-SSH feature is only usable with one session per remote host at a time. As long as the user opens exactly one session, it remains stable indefinitely — which strongly suggests the bug is specifically in the per-session bring-up logic on the remote side (spawning a fresh --serve instead of reusing the existing one), not in the steady-state path.
Concretely:
- ✅ 1 Desktop session ↔ 1 remote host: works, indefinitely.
- ❌ 2+ Desktop sessions ↔ same remote host: all hang.
- ✅ Workaround for users needing multiple sessions: SSH into the host in a regular terminal and run
claudethere (terminal CLI does not exhibit token churn).
Why this is distinct from existing issues
- #48530 — single-client SSH connection flood;
remote-server.logis empty because--servenever starts. Here--servestarts (multiple times) and the log clearly showsUnauthorized. - #27658 — multi-session cross-contamination, but explicitly local Desktop only, no SSH.
- #43252 — "shared connection" hang within a single session's Code/Chat tabs, not multiple sessions.
- #24317 — OAuth refresh race, unrelated to the per-host
--servetoken.
Suggested fix direction
The Desktop GUI should either:
- Reuse a single
--serve+ bridge per remote host across all sessions (preferred — also eliminates the SSH reconnect storm), or - When spawning a new
--serveis unavoidable, atomically migrate existing bridges to the new token instead of leaving them with a stale one and disconnecting them withUnauthorized.
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
Confirming this also breaks cross-machine concurrency, with a working server-side workaround
Same root cause hits a slightly different scenario: two physical machines running CCD against the same remote Linux host. Symptoms are identical (
Unauthorized request, hangs, reconnect storms) but the user-facing failure is more aggressive - opening CCD on machine B silently kicks the active session on machine A.Diagnosis on our side
Each CCD connect-sequence on the remote contains these steps (captured by wrapping
authorized_keyswithcommand="..."+ logging):So beyond the token-churn already described above, each client actively
pkills every other client's daemon and ccd-cli processes and SFTP-overwrites the server binary before bringing up its own. With two machines on different CCD versions (we had 2.1.119 vs 2.1.142), they end up in a perpetual war.Workaround: per-source isolation via
authorized_keyswrapperUntil the client is fixed to either share a single
--serveper host, or to use per-instance paths, we deployed a server-side wrapper that gives each source machine its own private~/.claude/remote-<name>/tree. Two SSH keys (one per machine), each with acommand="..."prefix inauthorized_keys:The wrapper (
ccd-isolate.sh):Effects
--servedaemon, socket, token file, server binary version, and ccd-cli treepkill -ffrom one machine no longer matches the other's processes (path is now unique)~/.claude/remote/plugins/(works in practice today; may need similar isolation if plugin formats diverge between CCD versions)Tradeoffs
sysctl kernel.apparmor_restrict_unprivileged_userns=0(or a named AppArmor profile) for theunshare --user --mountcall to work. The restriction was added in 24.04 by default~/.claude/remote/won't appear in unrelated commands — true in practice for CCD traffic but fragile in principlePosting this in case anyone else needs cross-machine concurrency before a proper client-side fix lands.
i think i experience this issue. or something very similar. was there a fix or workaround? i am on Claude 1.13576.4 (414f85). ubuntu24 in virtual machine on macos. claude code app from macos host.
New trigger: duplicate host registration causes token race on app launch, no session required
Same root cause as described in this issue — concurrent
--servegenerations rotating the token file out from under a live bridge — but hit via a distinct path that requires no user action beyond opening the app.Environment
Trigger
The Desktop app stores SSH connection entries per address. This host was registered twice: once by LAN IP and once by a VPN hostname (Tailscale). Both entries resolve to the same physical machine.
Current Desktop versions auto-connect all registered SSH hosts on app launch to fetch the directory tree. That means opening the app — with no session started, no second user-initiated connection — dials both entries simultaneously, spawning two concurrent
--servegenerations on a single host. The second spawn rotates the token file, invalidating the bridge from the first.This is a lower-friction path to the same race than the issue's original repro. The user doesn't need to manually open a second session; it happens invisibly on every app launch as long as the host has two entries.
Repro
--serveprocesses spawn with different--token-filehashes, no session started by the user.Unauthorizedonserver.ping,Connection closed,Lost connection to remote server.Evidence — failure state (observed during the failure window; server restarted as part of the fix)
Token race — two concurrent
--serve, distinct token-file hashes:<pid> ~T+0 server --serve --socket .../<sess>/rpc.sock --token-file .../token.<hash-A>
<pid> ~T+2min server --serve --socket .../<sess>/rpc.sock --token-file .../token.<hash-B>
Same socket path, different token-file names. The bridge from the first connection holds
<hash-A>; the second launch rotates it to<hash-B>.Unauthorized log line (from
remote-server.logduring failure):Unauthorized request: method=server.ping
Two-inode lsof — two processes bound to
rpc.sockon different inodes (orphan on the stale inode, live serve on the current one):server <pid-A> <user> 3u unix 0x<inode-stale> ... .../rpc.sock
server <pid-B> <user> 3u unix 0x<inode-live> ... .../rpc.sock
Note: captured live during the failure window. The server was restarted as part of the fix and logs rotated, so current logs show the clean post-fix state only.
Evidence — post-fix state (live)
After deleting one of the two duplicate registrations, a single
--serveis running with a stable token:$ ps aux | grep 'remote/srv' | grep -v grep
<pid> server --serve --socket .../<sess>/rpc.sock --token-file .../token.<hash>
<pid> server --bridge --socket .../<sess>/rpc.sock
$ lsof .../<sess>/rpc.sock
server <pid> <user> 3u unix 0x<inode> 0t0 .../rpc.sock
server <pid> <user> 7u unix 0x<inode> 0t0 .../rpc.sock
Single process, two FDs on the socket (normal for a Unix-socket server) — no orphan, no stale inode, no second generation.
Confirmed not a local watchdog
launchctl listandcrontab -lshow no cron or LaunchAgent respawning--serve. All respawns originated from the Desktop app's automatic host-connection on launch.Fix
Delete one of the two duplicate registration entries in the Desktop app → single
--serveon next launch → stable.Suggested direction
Aligns with the existing suggestion #1 in this issue: deduplicate registered connections that resolve to the same host before auto-connecting on launch, or reuse a single
--serve/bridge per physical host across connection entries regardless of the address string used to register it. The current behavior of launching a fresh--serveper connection entry — without checking whether the target is already serving — makes duplicate registrations silently destructive.i had a session with Claude Cowork debbugging the issue on my machine...
the conclusion and suggested fix was as described - to leave just one entry in ssh_configs.json, instead of 3 aliases i accumulated for ubuntu24 VM running on same macos. unfortunately it is still not fixed - claude code GUI in Claude macos starts a session to ubuntu24 VM and then loses the connection in about 12 seconds.
i am on latest Claude 1.15962.1 (1e236d) 2026-06-26T19:44:44.000Z
is it possible to share claude cowork chat where i tried to debug the problem?
Still reproducible on current builds, and with a single session — deliberate multi-session isn't required. Any client-side reconnect (session drop mid-response, app relaunch) spawns a new
--serve+ token while the previous--servepersists (PPID 1) and keeps squattingrpc.sock, so the reconnect getsUnauthorized request: method=server.pingand the app enters a respawn loop.Environment
| Component | Version |
|---|---|
| Remote:
ccd-cli(pushed by Desktop client) |2.1.197|| Remote:
~/.claude/remote/srv/<hash>/server|d20a77da22b7d4822f758654b226299ad7021c22(built 2026-06-11) || Remote OS | Ubuntu 22.04 LTS, x86_64, kernel 5.15 |
| Connection | Desktop app → GUI SSH → Linux VPS |
Observed cycle (~2 min period, repeats indefinitely)
--servewith new--token-file, launchesccd-clisession — works briefly (long enough to send a message and start streaming a response).--serve(still alive, detached at PPID 1) rejects it:--servespawns anyway → generations pile up (observed 3 concurrent--serveon onerpc.sock, every one holding a--token-filepath that no longer existed on disk).The user-visible symptom is "connected for a second, then disconnected while it was thinking," followed by a permanent reconnecting state.
Additional data points
sshd: user@notty) every ~15s during the loop without tearing down prior ones — 3-4 concurrent transport SSH sessions observed.--serve/--bridgeworkers + wiping~/.claude/remote/run/<id>/gives one clean session, but the first client-side reconnect re-wedges it.--serveexists, kills all but the newest — this breaks the wedge at the cost of murdering the older session (acceptable single-user, matches the token semantics anyway since old tokens are already invalidated).Suggested fix direction:
--serveshould exit when its token is superseded / its last channel dies, or the newly-spawned--serveshould reap prior generations on the same socket before binding.Confirming this bug is still present on the latest Claude Desktop (v1.18286.0) connecting to a remote Ubuntu host (deployed server
claude-ssh 7c2f88d1…, app-cached CLI v2.1.197, remoteclaudeCLI 2.1.116). Reproduced continuously, even after reducing to a single host profile.Client log (
~/Library/Logs/Claude/ssh.log) — the loop, verbatim:In a single session this produced
Reattach failed: auth error×284 andRPC stream closed×615. The capability handshake SUCCEEDS, then the RPC stream is closed ~1s later, and every reconnect fails to reattach (auth error), so it starts yet another fresh--serve.Server side confirms the churn: multiple
server --serve --socket …/rpc.sock --token-file …/token.<hex>processes racing on the SAME socket with DIFFERENT token files, and[Server] Unauthorized request: method=files.list / server.pingwhen an older server holds the socket while the client presents the newer token.Ruled out (so this sits squarely in the app's reattach/token logic):
USERAUTH_SUCCESS, connects fine~/.claude/remote/srv, re-deployed the same matching version, still failsImpact: the Desktop remote session is completely unusable for this host. Only the terminal CLI (
ssh host; claude) works, because it bypassesBinaryDeploymententirely.Suggested fix: make the reattach token stable across reconnects (persist/reuse the token the
--servewas started with), and when reattach fails, terminate the previous--servebound to that socket before starting a fresh one — instead of racing multiple servers on the same rpc socket.