github/copilot-sdk · error

timed out waiting for CLI process to exit after kill

Error message

timed out waiting for CLI process to exit after kill

What it means

After sending the kill signal, the library waits up to processExitTimeout for the process to actually exit. If the process ignores SIGKILL aftermath (or is stuck in an uninterruptible kernel state) and does not exit in time, this error is joined with any kill error. It indicates a hung child process that may need manual cleanup.

Solutions

  1. Find and manually kill the leftover process (ps / pgrep for the CLI binary, then kill -9).
  2. Check for orphaned child processes holding pipes open; kill the whole process group.
  3. If in a container, inspect dmesg for D-state tasks and restart the container if stuck.
  4. Report a persistent reproducible hang to the library maintainers with the CLI version.
Defensive patterns

Strategy: fallback

Try / catch

if err := client.Stop(ctx); err != nil {
    if strings.Contains(err.Error(), "timed out waiting for CLI process to exit") {
        log.Warn("CLI process hung after kill; manual cleanup required")
        // best-effort: pkill -9 the binary path
    }
    return err
}

Prevention

When it happens

Trigger: Calling Stop on a CLI process that fails to terminate within processExitTimeout after Kill() — typically a process stuck in uninterruptible I/O or a zombie held by a child.

Common situations: CLI process blocked on network I/O in a container, child processes inheriting file descriptors keeping the process group alive, NFS/D-state hangs.

Understand the failure class

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/aae72772fd767fdf. Report an issue: GitHub.

Appendix: source

Thrown at go/client.go:2382

			return fmt.Errorf("failed to kill CLI process: %w", err)
		}
	}
	c.process = nil
	return nil
}

func (c *Client) killProcessAndWait() error {
	done := c.processDone
	killErr := c.killProcess()
	if done == nil {
		return killErr
	}

	select {
	case <-done:
		return killErr
	case <-time.After(processExitTimeout):
		return errors.Join(killErr, fmt.Errorf("timed out waiting for CLI process to exit after kill"))
	}
}

// monitorProcess signals when the CLI process exits and captures any exit error.
// processError is intentionally a local: each process lifecycle gets its own
// error value, so goroutines from previous processes can't overwrite the
// current one. Closing the channel synchronizes with readers, guaranteeing
// they see the final processError value.
func (c *Client) monitorProcess() {
	done := make(chan struct{})
	c.processDone = done
	proc := c.process
	c.osProcess.Store(proc.Process)
	var processError error
	c.processErrorPtr = &processError
	go func() {
		waitErr := proc.Wait()
		var stderrOutput string

View on GitHub (pinned to cd8cf15dc3)