chenhg5/cc-connect · error
process tree (pid %d) 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.
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)
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 %d) still alive after SIGKILL retries: %w
- listen for Agy permission hooks: %w
- generate permission bridge token: %w
- resolve home directory for Agy permission bridge: %w
- create Agy permission overlay: %w
AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06).
Data as JSON: /api/errors/12ad2db95776a0a2.
Report an issue: GitHub.