github/copilot-sdk · critical

CLI process exited: \nstderr

Error message

CLI process exited: %w\nstderr: %s

What it means

The monitoring goroutine waits on the CLI process and, when Wait() returns an error (non-zero exit or failed wait), stores 'CLI process exited: <err>' with the captured stderr appended. This error is surfaced through the client's processError channel to RPC callers whose in-flight requests are then failed. It means the CLI server process died unexpectedly during operation.

Solutions

  1. Read the embedded stderr for the crash cause (panic trace, OOM message).
  2. Check system logs for OOM-killer events if stderr is empty but the exit code is 137.
  3. Upgrade the CLI/library pair; a known crash may be fixed in a newer version.
  4. Wrap RPC calls with retry-on-process-death logic that restarts the client.

Example fix

// before
resp, err := client.RPC.Call(ctx, "method", args)
// after
resp, err := client.RPC.Call(ctx, "method", args)
if err != nil && strings.Contains(err.Error(), "CLI process exited") {
    client = newClient(); client.Start(ctx) // restart and retry
}
Defensive patterns

Strategy: retry

Try / catch

_, err := client.RPC.Call(ctx, "method", args)
if err != nil && strings.Contains(err.Error(), "CLI process exited") {
    client = NewClient(cfg)
    if err := client.Start(restartCtx); err != nil { return err }
    return client.RPC.Call(ctx, "method", args) // one retry after restart
}

Prevention

When it happens

Trigger: The CLI server process crashes or exits non-zero while the client is active; the monitor captures waitErr plus any stderr output into processError, delivered via SetProcessDone/processErrorPtr.

Common situations: CLI running out of memory, hitting an internal panic, being OOM-killed by the OS, or exiting due to a fatal runtime error mid-session.

Related errors


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

Appendix: source

Thrown at go/client.go:2406

// 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
		if buf, ok := proc.Stderr.(*truncbuffer.TruncBuffer); ok {
			stderrOutput = strings.TrimSpace(buf.String())
		}
		if waitErr != nil {
			if stderrOutput != "" {
				processError = fmt.Errorf("CLI process exited: %w\nstderr: %s", waitErr, stderrOutput)
			} else {
				processError = fmt.Errorf("CLI process exited: %w", waitErr)
			}
		} else {
			if stderrOutput != "" {
				processError = fmt.Errorf("CLI process exited unexpectedly\nstderr: %s", stderrOutput)
			} else {
				processError = errors.New("CLI process exited unexpectedly")
			}
		}
		close(done)
	}()
}

// connectToServer establishes a connection to the server.
func (c *Client) connectToServer(ctx context.Context) error {
	if c.useStdio || c.useInProcess {
		// Already connected: stdio in startCLIServer, FFI streams in startInProcess.

View on GitHub (pinned to cd8cf15dc3)