anthropics / anthropics/claude-code

Remote SSH daemon destroys all running sessions on transient reconnect instead of reattaching

未关闭
#93,154 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
area:core bug has repro platform:linux platform:macos
主要语言
Python
星标
145k
派生
23.1k
PR 合并指标
PR 指标待抓取

描述

## Summary

On a **transient** SSH interruption, the desktop app's remote-session warm-up destroys a **healthy** remote daemon instead of reattaching to it, and the resulting `server.shutdown` SIGKILLs every tracked child session.

In our case one brief network blip cost **10 live `ccd-cli` agent sessions** (42 process groups including their children). The user did not close the app or the sessions.

What makes this look like a branch-selection bug rather than a missing capability: the **same** shipped binary already implements non-destructive daemon replacement on its *takeover* path (SIGTERM-first, children explicitly orphaned and surviving). Only the RPC shutdown path is unconditionally destructive.

## Versions

- Remote server: `claude-ssh 4534d864… (built 2026-09-02T00:57:03Z)`
- Remote CLI spawned per session: `ccd-cli 2.1.260`
- Remote host: Linux x86-64; client: Claude desktop app on macOS

## What happens

1. The SSH channel drops briefly. Client side logs `channel_closed_no_socket`, then warm-up fails with `bridge_startup_timeout`.
2. On that failure branch, `RemoteServerController` issues `server --stop` — i.e. the `server.shutdown` RPC — **without first checking whether the existing daemon is still alive and serving**.
3. The daemon's `server.shutdown` handler SIGKILLs all tracked child process groups. There is no SIGTERM stage, no grace period, and no active-session guard on this path.

## Evidence (remote server log, redacted)

The daemon was **still healthy and doing useful work** in the same second the shutdown arrived — note the `exit code 0` reap immediately before it, 104 seconds after the connection had dropped:

```
16:46:12 [Server] writeResponse: wrote 0/54 bytes, error=... use of closed network connection
16:46:12 [frameSink] write failed, detaching: ... use of closed network connection
---- 104 seconds of silence; daemon alive throughout ----
16:47:56 [process.Manager] Process exited with code 0 <-- healthy, still reaping
16:47:56 [Server] New connection from: @
16:47:56 [ServerHandler] server.shutdown received over RPC
16:47:56 [Server] shutdown requested
16:47:56 [process.Manager] Process exited with code -1, terminated by SIGKILL, signalled at shutdown request
... (repeated for every tracked child) ...
16:47:56 [Server] cleanup: closed 1 connection(s), killed 42 child process group(s)
```

Blast radius for that single event: **10 `ccd-cli` session processes** SIGKILLed outright, 42 process groups total.

Control observation on the same machine: a second daemon belonging to a different account has run **continuously for 2 days** under the same desktop app with zero shutdowns. So the destructive stop is specifically the warm-up **failure** branch, not ordinary reconnect — ordinary reconnect already reuses the running daemon correctly.

## The non-destructive path already exists

The daemon-vs-daemon takeover path in the shipped binary escalates gracefully and explicitly preserves children — this log template is present in the binary:

```
[daemon] %s: run dir is held by a live daemon, %s; sending SIGTERM
[daemon] %s: previous daemon %s exited after SIGTERM
[daemon] %s: WARNING previous daemon %s ignored SIGTERM for %s; sending SIGKILL
(its Claude Code children, if any, are orphaned)
```

The RPC path taken by `server --stop` has no equivalent — it goes straight to SIGKILL of every tracked child.

## No user-side mitigation exists

We looked for one before filing. As far as we can tell there is no supported knob anywhere:

- Server flag surface is `-serve -bridge -stop -socket -token-file -install -cli-* -version` — no reuse/reattach/grace/probe-budget flag.
- No environment variable appears to gate the shutdown or warm-up path.
- The daemon reads no configuration file beyond the documented token file and socket path.
- The RPC surface exposes `server.shutdown` and `server.ping` plus `process.*` / `files.*` / `git.status` — there is no `drain`, `detach`, `reload`, or `keepalive` method, and no way for the server to refuse or defer a shutdown while sessions are active.
- The SSH-sessions documentation covers host/port/identity and enterprise host allowlists only; nothing on reconnect lifecycle, retry budgets, or session durability.

We deliberately did not work around it (no binary wrapper, no forced SSH command, no detachment shim), because any of those would fight the product rather than fix it.

## Requested behaviour

On warm-up failure, probe the existing daemon before destroying it — the pieces are already there:

- `server.ping` RPC for liveness, and
- the `daemon.lock` record, which already carries `pid`, `instanceId` and `startedAt`.

If the daemon answers, reattach instead of issuing `server.shutdown`. If a stop is genuinely required, apply the same SIGTERM-first / orphan-children semantics the takeover path already uses, or add an active-session guard so a stop cannot silently destroy running work.

Failing that, exposing the choice as a setting would be enough for us.

## Impact

We run long-lived agent sessions on a remote host. A single transient network blip currently discards all of them with no warning and no recovery path, and because it is triggered by network conditions rather than user action it is not something the user can avoid.

贡献指南

这个仓库没有索引到贡献指南

调研方向

Start from the remote-session warm-up path in RemoteServerController and the branch that calls `server --stop` after `bridge_startup_timeout`. Read the RPC handlers for `server.shutdown` and `server.ping`, plus the `daemon.lock` handling for pid, instanceId and startedAt. Done means a transient reconnect probes and reattaches to a live daemon, or any required stop uses non-destructive/guarded shutdown semantics.

由索引模型根据 Issue 内容生成。

评估

技术栈
linux, macos
领域
backend, cli
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
活跃
描述清晰度
基本清楚
新手友好度
45/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。