[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.
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗