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

  1. 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.
  2. Ensure the parent reaps children: verify Close/waitToExit reaps the process (cmd.Wait) so zombies do not keep the pid 'alive'.
  3. In containers, kill from inside the same PID namespace; killing a namespace-mapped pid from the host can miss the real process.
  4. If PID reuse is suspected, compare the process name/cmdline against the expected agent binary before concluding it is still alive.
  5. 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

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


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/12ad2db95776a0a2. Report an issue: GitHub.