[BUG] Desktop SSH: quitting the app opens N simultaneous ssh connections (N = sessions on the host, archived included) instead of using the existing control socket
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When Claude Desktop (macOS) quits while it has SSH sessions in the Code tab, it spawns one independent /usr/bin/ssh process per session transcript on the remote host (including archived sessions) to read the .jsonl files (wc -c / head -c 65536, tail -c +N, ls .../subagents/agent-*.jsonl). These invocations do not use the control socket the app already maintains for that host (-S .../ccd-ssh-XXXX/ctl), so each one is a full TCP connection with its own SSH handshake and authentication.
With ~30 sessions on the host this produces ~30 SSH connections in roughly 300 ms. My university's perimeter firewall treats this as an SSH brute-force attempt and blocks my IP, so the next launch of Claude Desktop fails to connect at all.
For comparison, app startup is well behaved: one ControlMaster=yes master per SSH connection profile (2 in my case), and the sftp / server --bridge channels reuse it via -S. Only the transcript reads on quit bypass the multiplexing.
Note: these transcript-read ssh invocations are not passed -F /dev/null and do not override ControlMaster/ControlPath, so they honour the user's ~/.ssh/config. If a user-owned ControlMaster socket for the host already exists at that moment, they multiplex through it; if it doesn't, all ~30 race to become master and fall back to independent connections. The masters spawned by the app itself pass -o ControlMaster=yes -S <private socket> (and, on the current version, -o ProxyCommand=none), so user config cannot route the app's own connections.
What Should Happen?
- Transcript reads (and any other per-session remote command) should go through the existing per-host control socket (
-S .../ccd-ssh-XXXX/ctl -o ControlMaster=no), exactly like thesftpandserver --bridgechannels already do. - Alternatively / additionally, the number of concurrent SSH connections per host should be bounded (e.g. batch the reads through a single remote command, or cap concurrency).
- Archived sessions should probably not be touched at all on quit.
Error Messages/Logs
### Outbound SYNs to the remote host (port 2222) captured on the client during app quit
sudo tcpdump -ni any 'tcp dst port 2222 and tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'
# App launch: 4 SYNs over ~5 s (expected: 2 masters + channels)
17:04:25.272098 IP CLIENT.55372 > REMOTE.2222: Flags [SEW] ...
17:04:25.474030 IP CLIENT.55383 > REMOTE.2222: Flags [SEW] ...
17:04:28.254888 IP CLIENT.55423 > REMOTE.2222: Flags [SEW] ...
17:04:30.741843 IP CLIENT.55426 > REMOTE.2222: Flags [SEW] ...
# App quit (Cmd+Q): 30 SYNs between 17:04:39.631 and 17:04:39.933
17:04:39.631790 IP CLIENT.55432 > REMOTE.2222: Flags [SEW] ...
17:04:39.631790 IP CLIENT.55431 > REMOTE.2222: Flags [SEW] ...
17:04:39.634644 IP CLIENT.55433 > REMOTE.2222: Flags [SEW] ...
... (26 more, source ports 55434–55459) ...
17:04:39.933026 IP CLIENT.55460 > REMOTE.2222: Flags [SEW] ...
### ssh processes spawned by the app during quit
Captured with a 100 ms polling loop over `ps -ax -o pid,lstart,command | grep '[/]usr/bin/ssh'`. All of the following were spawned within the same second. Paths are representative; there were ~30 distinct `.jsonl` files across several project directories and worktrees, many belonging to archived sessions.
/usr/bin/ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -- HOSTALIAS sh -c 'LC_ALL=C sh -c '\''wc -c < "$1" && head -c 65536 "$1"'\'' sh '\''/home/USER/.claude/projects/-home-USER-PROJECT/<uuid>.jsonl'\'''
/usr/bin/ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -- HOSTALIAS sh -c 'LC_ALL=C tail -c +2681727 '\''/home/USER/.claude/projects/-home-USER-PROJECT/<uuid>.jsonl'\'''
/usr/bin/ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -- HOSTALIAS sh -c 'ls -1 '\''/home/USER/.claude/projects/-home-USER-PROJECT'\''/'\''<uuid>'\''/subagents/agent-*.jsonl '\''/home/USER/.claude/projects/-home-USER-PROJECT'\''/agent-*.jsonl 2>/dev/null'
Note the absence of `-S` / `-o ControlMaster=no` on these, compared to the channels the app opens at startup:
# per-profile master (one per SSH connection profile) — fine
/usr/bin/ssh -o Hostname=REMOTE -l USER -N -T -o ControlMaster=yes -o ControlPersist=no -p 2222 ... -S /var/folders/.../T/ccd-ssh-XXXX/ctl ... -- HOSTALIAS
# channels reusing the master — fine
/usr/bin/ssh -s -F /dev/null -T -o ControlMaster=no -S /var/folders/.../T/ccd-ssh-XXXX/ctl ... -- HOSTALIAS sftp
/usr/bin/ssh -F /dev/null -T -o ControlMaster=no -S /var/folders/.../T/ccd-ssh-XXXX/ctl ... -- HOSTALIAS '/home/USER/.claude/remote/srv/<hash>/server' '--bridge' '--socket' '...'
Steps to Reproduce
- On macOS, add an SSH connection in the Code tab to a Linux host and create ~20–30 sessions against it over time (some archived, some active).
- Start
sudo tcpdump -ni any 'tcp dst port <ssh-port> and tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'on the Mac. - Launch Claude Desktop; observe a handful of SYNs.
- Quit Claude Desktop with Cmd+Q; observe ~N SYNs (N ≈ number of session transcripts on the host) within a few hundred milliseconds.
- Optionally, on the remote host,
journalctl -u sshshows the corresponding burst ofAccepted publickeylines.
Verified with tcpdump: app launch produces 1–3 SYNs, switching to the Code tab / opening sessions / sending messages / 5 min idle produce 0, and each Cmd+Q produces 27–44 SYNs within ~300 ms.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude Desktop 1.44121.0 (a670de) 2026-09-01T00:22:02.000Z Remote ccd-cli: 2.1.255 (installed by Desktop on the remote host)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Impact
- Any host behind a rate-limiting firewall, fail2ban-style tooling, or sshd
MaxStartupswill throttle or ban the client IP. - In my case the university firewall bans the IP after the quit burst, so the next launch can't connect, which looks like a startup problem but is actually caused by the previous shutdown.
Workaround
Pre-establishing a user-owned ControlMaster for the host before launching the app (so the transcript reads multiplex through it):
Host HOSTALIAS
ControlMaster auto
ControlPath ~/.ssh/cm/%r@%h:%p
ControlPersist yes
then ssh -Nf HOSTALIAS before opening Claude Desktop. This works only if the master already exists when the burst happens, and only for these transcript reads; the app's own masters ignore user config.
Related Issues
- \#48530 — Desktop client floods server with SSH sessions during connection setup, triggers MaxStartups (closed; startup path now uses a control socket, but the quit-time transcript reads still bypass it)
- \#54179 — repeated
Accepted publickeyevery 5–10 s with multiple sessions on the same host