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

  1. Run the CLI manually to see its real startup failure (exit code, stderr)
  2. Check Event Viewer and antivirus logs for process termination
  3. Validate the Job Object configuration doesn't kill the process prematurely
  4. 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

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


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)