chenhg5/cc-connect · error
process tree (pid ) still alive 10s after SIGKILL reported…
Error message
process tree (pid %d) still alive 10s after SIGKILL reported success
What it means
This error is returned by (*ClaudeSession).Close after the session has sent SIGKILL to the agent process and, despite the kill reporting success, the process (or its process tree) was still alive after a 10-second grace period. It indicates an unkillable or restarted child process, typically a zombie state or a process stuck in uninterruptible kernel sleep (D state). The library throws it so callers know cleanup did not actually succeed and resources (pid, pipes) may still be held.
Solutions
- Check the process state (ps -o stat= -p <pid>); if in D state, resolve the blocking I/O (NFS/FS hang) or reboot the host — SIGKILL cannot preempt D state.
- Ensure the parent reaps children: verify Close/waitToExit reaps the process (cmd.Wait) so zombies do not keep the pid 'alive'.
- In containers, kill from inside the same PID namespace; killing a namespace-mapped pid from the host can miss the real process.
- If PID reuse is suspected, compare the process name/cmdline against the expected agent binary before concluding it is still alive.
- As a last resort, log the pid and continue: the error is advisory for cleanup, and the OS will eventually reclaim the process.
Example fix
// before
if err := cs.Close(); err != nil {
return fmt.Errorf("close failed: %w", err)
}
// after
if err := cs.Close(); err != nil {
var stillAlive *os.ProcessState // advisory: process may be in D-state
slog.Warn("claude session close did not fully terminate process", "err", err)
// do not retry SIGKILL in a loop; investigate pid state out-of-band
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before closing, confirm the process exists and check its state
out, _ := exec.Command("ps", "-o", "stat=", "-p", strconv.Itoa(pid)).Output()
if strings.HasPrefix(strings.TrimSpace(string(out)), "D") {
slog.Warn("agent process in uninterruptible sleep; kill may not complete")
} Type guard
func isStillAlive(p *os.Process) bool {
return p != nil && p.Signal(syscall.Signal(0)) == nil
} Try / catch
if err := session.Close(); err != nil {
if strings.Contains(err.Error(), "still alive 10s after SIGKILL") {
slog.Warn("process not reaped; inspect pid state out-of-band", "err", err)
// do not tight-loop retry; schedule a delayed recheck
} else {
return err
}
} Prevention
- Always let Close complete its wait/reap logic; never SIGKILL children manually in parallel.
- Monitor for processes stuck in D state on hosts using network filesystems.
- Run in the same PID namespace as the agent when using containers.
- Set reasonable timeouts and log pids so stuck processes can be investigated.
When it happens
Trigger: Calling Close() on a ClaudeSession whose underlying cmd process was sent SIGKILL, the kill syscall returned success, but a subsequent liveness check (e.g. signal 0 or /proc lookup) found the pid still alive 10 seconds later. Also exercised by TestClaudeSessionClose_IdempotentNoPanic when Close is invoked on sessions with live/stale process handles.
Common situations: Claude CLI process hung in uninterruptible I/O (D state) so SIGKILL cannot take effect; a zombie child whose parent has not reaped it; container/namespace setups where the pid belongs to a different namespace; PID reuse causing the liveness probe to check an unrelated process.
Related errors
- process tree (pid ) still alive after SIGKILL retries
- -2
- agent error (no details)
- agent process exited
- agent process exited during compress
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/12ad2db95776a0a2.
Report an issue: GitHub.
Appendix: source
Thrown at agent/claudecode/session.go:1311
case <-cs.done:
slog.Info("claudeSession: exited during force-kill retries")
return nil
case <-time.After(2 * time.Second):
}
}
select {
case <-cs.done:
return nil
case <-time.After(10 * time.Second):
pid := -1
if cs.cmd != nil && cs.cmd.Process != nil {
pid = cs.cmd.Process.Pid
}
if killErr != nil {
return fmt.Errorf("process tree (pid %d) still alive after SIGKILL retries: %w", pid, killErr)
}
return fmt.Errorf("process tree (pid %d) still alive 10s after SIGKILL reported success", pid)
}
}
// shellJoinArgs joins args into a single string, quoting any arg that
// contains whitespace so that a shell-style splitter (like my_cli's
// splitCommandLine) preserves each arg as one token.
//
// Uses single quotes because some splitters (e.g. my_cli) don't support
// backslash escapes inside double quotes. For values containing single
// quotes, we close the single-quoted segment, add an escaped single
// quote, and reopen: 'it'\”s' → it's
func shellJoinArgs(args []string) string {
var b strings.Builder
for i, a := range args {
if i > 0 {
b.WriteByte(' ')
}
if !strings.ContainsAny(a, " \t\n\r'\"\\") {View on GitHub (pinned to 4000b2338a)