[BUG] Desktop GUI SSH: opening multiple Claude Code sessions on the same remote host invalidates each other's auth token, all sessions hang

Open 💬 6 comments Opened Apr 28, 2026 by songoow

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

  1. Launch macOS Claude Code Desktop.
  2. 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. ✅
  3. While the first session is still open, open a second session in the Desktop GUI against the same remote host (any project).
  4. 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:

  1. On the Mac: Cmd+Q to 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).
  2. 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.*
``

  1. 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 claude there (terminal CLI does not exhibit token churn).

Why this is distinct from existing issues

  • #48530 — single-client SSH connection flood; remote-server.log is empty because --serve never starts. Here --serve starts (multiple times) and the log clearly shows Unauthorized.
  • #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 --serve token.

Suggested fix direction

The Desktop GUI should either:

  1. Reuse a single --serve + bridge per remote host across all sessions (preferred — also eliminates the SSH reconnect storm), or
  2. When spawning a new --serve is unavoidable, atomically migrate existing bridges to the new token instead of leaving them with a stale one and disconnecting them with Unauthorized.

View original on GitHub ↗

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