{"record":{"id":"aae72772fd767fdf","repo":"github/copilot-sdk","slug":"timed-out-waiting-for-cli-process-to-exit-after-ki","errorCode":null,"errorMessage":"timed out waiting for CLI process to exit after kill","messagePattern":"timed out waiting for CLI process to exit after kill","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/client.go","lineNumber":2382,"sourceCode":"\t\t\treturn fmt.Errorf(\"failed to kill CLI process: %w\", err)\n\t\t}\n\t}\n\tc.process = nil\n\treturn nil\n}\n\nfunc (c *Client) killProcessAndWait() error {\n\tdone := c.processDone\n\tkillErr := c.killProcess()\n\tif done == nil {\n\t\treturn killErr\n\t}\n\n\tselect {\n\tcase <-done:\n\t\treturn killErr\n\tcase <-time.After(processExitTimeout):\n\t\treturn errors.Join(killErr, fmt.Errorf(\"timed out waiting for CLI process to exit after kill\"))\n\t}\n}\n\n// monitorProcess signals when the CLI process exits and captures any exit error.\n// processError is intentionally a local: each process lifecycle gets its own\n// error value, so goroutines from previous processes can't overwrite the\n// current one. Closing the channel synchronizes with readers, guaranteeing\n// they see the final processError value.\nfunc (c *Client) monitorProcess() {\n\tdone := make(chan struct{})\n\tc.processDone = done\n\tproc := c.process\n\tc.osProcess.Store(proc.Process)\n\tvar processError error\n\tc.processErrorPtr = &processError\n\tgo func() {\n\t\twaitErr := proc.Wait()\n\t\tvar stderrOutput string","sourceCodeStart":2364,"sourceCodeEnd":2400,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/go/client.go#L2364-L2400","documentation":"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.","triggerScenarios":"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.","commonSituations":"CLI process blocked on network I/O in a container, child processes inheriting file descriptors keeping the process group alive, NFS/D-state hangs.","solutions":["Find and manually kill the leftover process (ps / pgrep for the CLI binary, then kill -9).","Check for orphaned child processes holding pipes open; kill the whole process group.","If in a container, inspect dmesg for D-state tasks and restart the container if stuck.","Report a persistent reproducible hang to the library maintainers with the CLI version."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := client.Stop(ctx); err != nil {\n    if strings.Contains(err.Error(), \"timed out waiting for CLI process to exit\") {\n        log.Warn(\"CLI process hung after kill; manual cleanup required\")\n        // best-effort: pkill -9 the binary path\n    }\n    return err\n}","preventionTips":["Sweep for leftover CLI processes (pgrep) after abnormal shutdowns.","Kill the whole process group, not just the parent, when cleaning up.","Investigate D-state hangs at the OS level instead of retrying kills.","Watch for orphaned children holding inherited pipe fds."],"tags":["process","kill","timeout","hang"],"backgroundTag":"process-kill-failed","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}