github/copilot-sdk · error
CLI exited before thread resume
Error message
CLI exited before thread resume
What it means
After assigning the suspended CLI process to the Job Object, attach_and_resume must resume its initial thread. child.id() returning None means the process already exited before resume, so the code raises NotFound rather than calling resume with a dead PID.
Solutions
- Run the CLI manually to see its real startup failure (exit code, stderr)
- Check Event Viewer and antivirus logs for process termination
- Validate the Job Object configuration doesn't kill the process prematurely
- Reinstall/update the Copilot CLI to a compatible version
Example fix
// before
let tree = process_tree::spawn(cmd)?; // NotFound: CLI exited before thread resume
// after
match process_tree::spawn(cmd) {
Err(e) if e.kind() == io::ErrorKind::NotFound => {
eprintln!("CLI failed to start; verify installation: {e}");
std::process::exit(1);
}
r => r?,
} Defensive patterns
Strategy: validation
Validate before calling
// Rust
if !cli_binary_runs(cmd) { return Err(io::Error::new(io::ErrorKind::NotFound, "CLI binary cannot start")); } Try / catch
// Rust
if let Err(e) = process_tree::spawn(cmd) {
if e.kind() == io::ErrorKind::NotFound { log_cli_exit_reason(); }
return Err(e);
} Prevention
- Run the CLI manually to capture its real startup error
- Review Windows Event Viewer for silent process terminations
- Validate Job Object limit flags don't kill the child early
- Reinstall/update the CLI if startup is consistently instant-fail
When it happens
Trigger: The suspended CLI process dies between AssignProcessToJobObject and the thread-resume step — an immediate crash, missing dependency, or external termination (antivirus, job limits).
Common situations: Broken CLI installation on Windows; security software killing the process; Job Object limits (e.g. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE misconfiguration) terminating it early; incompatible CLI binary.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- CLI exited before Job Object assignment
- CLI initial thread was not found
- checking existing permissions
- CLI process not started
- Concurrent extraction race: target already exists but is…
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/bfced1f535d7812e.
Report an issue: GitHub.
Appendix: source
Thrown at rust/src/process_tree.rs:140
)
} == 0
{
return Err(io::Error::last_os_error());
}
let process = child.raw_handle().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"CLI exited before Job Object assignment",
)
})?;
// SAFETY: both handles are valid and the child is still suspended.
if unsafe { AssignProcessToJobObject(job.0, process.cast()) } == 0 {
return Err(io::Error::last_os_error());
}
resume_initial_thread(child.id().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "CLI exited before thread resume")
})?)?;
Ok(Tree { job })
}
fn resume_initial_thread(pid: u32) -> io::Result<()> {
// SAFETY: the returned snapshot handle is owned and closed below.
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let snapshot = OwnedHandle(snapshot);
let mut entry = THREADENTRY32 {
dwSize: size_of::<THREADENTRY32>() as u32,
..Default::default()
};
// SAFETY: `entry` has the documented size and remains live throughout
// enumeration.View on GitHub (pinned to cd8cf15dc3)