[BUG] Detached remote agent processes outlive their session with no Stop control and starve the host
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?
Severity: high, this caused a production outage.
Remote agent processes (ccd-cli) are designed to survive transport loss (entering detached state (grace=900000ms)) and to be re-attached on reconnect. But when the session's chat is gone from the UI there is no Stop button and no sidebar entry, so the only way out is quitting the app, and the process keeps running on the server with nothing left that can reach or stop it.
Across several such sessions these processes accumulated on one host until it was starved. sshd stopped completing handshakes and a co-hosted production service went down for about 20 minutes. The host was unreachable by any SSH client, not just by the app.
Environment (remote side)
- Claude desktop app on macOS (Darwin 25.6.0), Apple Silicon
- Remote server binary
5db5e4a12f88487e47c2c48259b69a2d630bb3f7,ccd-cli 2.1.219 - Remote host: Ubuntu,
linux-amd64, reached over SSH on a residential uplink (~170 ms RTT), through a VPN on the client side - The remote host also runs a production service, it is not a scratch box
- Log analysed:
~/Library/Logs/Claude/ssh.log, spanning 2026-06-14 to 2026-08-03
Impact observed
sshdon the remote host stopped completing SSH handshakes. TCP connections reachedESTABLISHEDbut no SSH banner was ever sent (verified with a 20-second rawncprobe). Four connections were stuck inFIN_WAIT_2. ICMP replied 4/4, so the kernel was healthy and the SSH service was not.- A third-party SSH client (Termius) could not open new sessions either.
- The production service on the host stopped responding.
- Recovery was immediate the moment I pressed Stop in the one old session I could still reach. SSH and the service came back at once.
Accumulation across sessions
17 ccd-cli agent sessions were spawned on this host over the log's lifetime. Six have no Exited line at all:
2026-08-02 15:17:04
2026-08-02 17:12:46
2026-08-02 21:08:01
2026-08-02 21:47:17
2026-08-03 00:53:57 <- the one that took the host down
2026-08-03 02:02:39 <- still current at the time of writing
A missing Exited line does not by itself prove the process is still alive: if the app was quit, it could not log the exit. That is precisely the problem, the app cannot account for these processes. It also twice found more than one alive at reconnect time (Rebinding 2 live process(es)).
Contributing factors visible in the same log
- The heartbeat shares the channel with bulk data.
server.pingqueues behind plugin-sync traffic and times out after 2 misses (~7 s), so a healthy but busy transport gets declared dead. Log-wide: 371 heartbeat misses, 63 transport deaths, 546 auto-reconnect attempts. - Each reconnect redoes full setup: a binary check plus a 306-file / 9.7 MB plugin sync, from scratch, with no resume. On this link a single reconnect costs 8-11 s and the sync cannot finish inside the heartbeat window, so it never converges. This is a livelock: sync saturates the channel, ping times out, transport is killed, sync restarts at zero.
- No resource isolation. The agent runs as the same user, on the same host as production services, with no cgroup or ulimit. Nothing prevents it from starving them.
- Passphrase-protected keys are silently skipped, with only an info-level line:
Skipping identity file ~/.ssh/<key>: Encrypted private OpenSSH key detected, but no passphrase given. Auth then depends on the key happening to be in the OS ssh-agent. There is no prompt and no warning, so if the agent does not hold the key, auth fails with no indication of why.
Why this is worse than plain SSH
Working through a terminal, a runaway process dies with its session, and I can always open another session and kill it. Here the process is deliberately made to outlive the connection, which is reasonable for a flaky link, but the control handle is not preserved with it. The result is a process that the user can neither see nor stop, on a machine they care about.
What Should Happen?
- A remote session should always have a reachable Stop control, and should stay in the sidebar for as long as its remote process may be alive.
- If a session's UI is discarded (app quit, chat gone), its remote process should be terminated. At minimum it should be surfaced on the next connect as "N orphaned remote processes on
<host>", with a way to kill them. - The detach grace (
900000ms) should be enforced as a hard ceiling by the remote side itself, not only tracked by a client that may never come back.
Concrete suggestions:
- Give the remote process a self-imposed deadline: if no client re-attaches within the grace period, it exits on its own.
- List live and orphaned remote processes per host in the UI, always killable.
- Put the heartbeat on its own channel, or scale its timeout to measured RTT, so bulk transfers cannot trigger a false "transport dead".
- Make plugin sync incremental and resumable, instead of restarting 306 files from zero on every reconnect.
- Offer an opt-in resource cap (cgroup or systemd slice) for the remote agent, so it cannot starve co-hosted services.
- Prompt for the passphrase of an encrypted
IdentityFile, or warn visibly instead of skipping it silently.
Error Messages/Logs
The process that caused the outage:
00:53:57 Creating process: /home/<user>/.claude/remote/ccd-cli/2.1.219 [27 arg(s)]
Working directory: /home/<user>/<project>
01:25:30 Transport closed; entering detached state (grace=900000ms)
01:25:40 Reattached (running=true, lastSeq=1329)
01:30:14 Transport closed; entering detached state (grace=900000ms)
01:30:24 Reattached (running=true, lastSeq=1331)
01:40:27 Transport closed; entering detached state (grace=900000ms)
It ran for about an hour. Its output sequence froze at 1331: it emitted essentially nothing after 01:25 while still being reported as running=true.
The host going down:
01:41:45 Connected to remote server (8381ms, trigger: warm_up, reused: true)
01:48:08 RemotePluginSync: Synced 187 file(s) <- partial, of 306
01:52:38 heartbeat failed 2 times; declaring transport dead and forcing close
01:53:09 Connection error: Timed out while waiting for handshake (30036ms)
... every subsequent attempt times out at exactly 30 s
Steps to Reproduce
- Connect the Claude desktop app to a remote host over SSH on a high-latency link (~170 ms RTT here), with plugins enabled so that every connect triggers a full plugin sync (306 files / 9.7 MB in my case).
- Start a remote agent session and give it a long-running task.
- Let the transport drop. On this link it drops on its own, and the log shows
Transport closed; entering detached state (grace=900000ms). - While the session is detached, quit the app or otherwise lose the chat from the sidebar.
- Reopen the app. The session is not in the sidebar and has no Stop control, but
ccd-cliis still running on the host. - Repeat across several sessions. The processes accumulate. Here: 17 spawned, 6 with no
Exitedline, and eventuallysshdstopped completing handshakes and co-hosted services stopped responding.
Claude Model
None
Is this a regression?
I'm not sure
Last Working Version
No response
Claude Code Version
Claude 1.24012.9 (03c61d) 2026-07-24T04:59:17.000Z
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
No response