xai-org/grok-build · error · std::io::Error

spawn shell in {}: {e}

Error message

spawn shell in {}: {e}

What it means

When spawning a shell process via Command::spawn fails, the raw io::Error is wrapped with the working directory for context: "spawn shell in {cwd}: {e}". On Unix the process is also attached to a new ProcessGroup, so the error means the shell binary could not be launched (or setup failed) before any session existed.

Source

Thrown at crates/codegen/xai-grok-tools/src/computer/local/terminal.rs:3258

        crate::util::apply_grok_agent_marker(&mut cmd);

        // Flags set inline: tokio's creation_flags is a SET, not OR, so the detach
        // helpers don't compose. CREATE_BREAKAWAY_FROM_JOB fails with os error 5 when
        // the parent's job lacks JOB_OBJECT_LIMIT_BREAKAWAY_OK; the caller retries without it.
        let mut flags = CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP;
        if with_breakaway {
            flags |= CREATE_BREAKAWAY_FROM_JOB;
        }
        cmd.creation_flags(flags.0);
        cmd
    };

    #[cfg(unix)]
    let mut group = crate::util::ProcessGroup::new()?;
    #[cfg(unix)]
    #[allow(clippy::disallowed_methods)] // attached to the process group built above
    let child = cmd.spawn().map_err(|e| {
        std::io::Error::new(e.kind(), format!("spawn shell in {}: {e}", cwd.display()))
    })?;

    #[cfg(not(unix))]
    #[allow(clippy::disallowed_methods)] // attached to the process group built in this block
    let (child, mut group) = {
        let group = crate::util::ProcessGroup::new()?;
        let mut cmd = build_cmd(true);
        match cmd.spawn() {
            Ok(child) => (child, group),
            Err(e) if e.raw_os_error() == Some(5) => {
                // Job disallows breakaway: retry without the flag. attach() below
                // will also fail, but kill_on_drop still reaps the immediate child.
                tracing::debug!(
                    "spawn with CREATE_BREAKAWAY_FROM_JOB returned ERROR_ACCESS_DENIED; \
                     retrying without breakaway (process-tree teardown disabled for this child)"
                );
                drop(cmd);
                let mut cmd = build_cmd(false);

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Check the error's inner kind/message and verify the shell binary exists and is executable (e.g. ls -l $(which bash)).
  2. Ensure the cwd passed to the spawn exists and is accessible; create it before spawning.
  3. Check process/thread limits (ulimit -u, container pids limit) if the cause is EAGAIN/EAGAIN-like resource exhaustion.
  4. Log and surface e.kind() to distinguish NotFound vs PermissionDenied vs Other.

Example fix

// before
let child = cmd.spawn().map_err(...)?;
// after
std::fs::create_dir_all(cwd)?;
let child = cmd.spawn().map_err(|e| {
    std::io::Error::new(e.kind(), format!("spawn shell in {}: {e}", cwd.display()))
})?;
Defensive patterns

Strategy: validation

Validate before calling

let shell = resolve_shell(); // e.g. /bin/bash
debug_assert!(std::path::Path::new(&shell).exists());
std::fs::create_dir_all(cwd)?;
if !cwd.is_dir() { return Err(anyhow!("cwd {:?} is not a directory", cwd)); }

Try / catch

match spawn_shell(cwd) {
    Ok(child) => child,
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
        bail!("shell binary missing on this image; install bash or fix config")
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: spawn returning Err — typically because the shell executable does not exist or lacks execute permission, cwd does not exist, fork/exec limits (EAGAIN) are hit, or resource limits (RLIMIT_NPROC) prevent process creation.

Common situations: Configured shell path wrong (e.g. /bin/zsh absent on the image); session cwd deleted or never created; running in a container with a low process limit; permission errors after restrictive chmod on the binary.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/5171437d24e9f95c. Report an issue: GitHub.