github/copilot-sdk · critical

CLI process exited unexpectedly\nstderr

Error message

CLI process exited unexpectedly\nstderr: %s

What it means

This error is stored by the Client when the CLI subprocess terminates while the SDK expects it to keep serving the JSON-RPC connection. The client waits on the process and, once Wait() returns, records either 'CLI process exited: <wait error>' (with captured stderr appended when available) or 'CLI process exited unexpectedly' when the process died without a wait error. The error is surfaced through processDone/processErrorPtr to the JSON-RPC client so pending and new calls fail with the underlying reason.

Solutions

  1. Read the stderr portion of the message to find the CLI's own fatal error and fix the underlying cause (bad flag, config, or version).
  2. Verify the CLI binary path/version is correct and runnable (run it manually with the same args).
  3. Check system logs for OOM kills or external signals that terminated the process.
  4. Handle the error via the processDone/processError channel and restart the client with fresh state instead of reusing the dead process.

Example fix

// before
c := client.New(client.WithCommand("co-pilot", "serve"))
res, _ := c.SomeRPC(ctx, req) // may fail: CLI process exited unexpectedly

// after
if err := exec.Command("co-pilot", "serve", "--version").Run(); err != nil {
    log.Fatalf("CLI binary missing or broken: %v", err)
}
res, err := c.SomeRPC(ctx, req)
if err != nil {
    select {
    case <-c.ProcessDone():
        c = restartClient() // process died; recreate
    default:
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

res, err := c.SomeRPC(ctx, req)
if err != nil {
    select {
    case <-c.ProcessDone():
        // process died; inspect err for stderr tail, restart client
    default:
    }
    return fmt.Errorf("rpc failed: %w", err)
}

Prevention

When it happens

Trigger: Calling any Client RPC after (or while) the spawned CLI process terminates: the process crashes, is killed (OOM, SIGKILL), exits due to a startup failure, or closes stdin/stdout prematurely. Emitted at the end of the wait goroutine when waitErr is nil but the process has terminated with stderr output, or with a wrapped waitErr otherwise.

Common situations: Wrong CLI binary path or version that exits immediately; the CLI crashing on startup due to bad config; OOM killer terminating the process; user or OS sending SIGKILL/SIGTERM; CLI hitting a fatal internal error and printing a stack trace to stderr.

Related errors


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

Appendix: source

Thrown at go/client.go:2412

	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.
		return nil
	}

	// Connect via TCP
	return c.connectViaTCP(ctx)
}

View on GitHub (pinned to cd8cf15dc3)