nikivdev/code · error · anyhow::Error

codex app-server closed stdout unexpectedly

Error message

codex app-server closed stdout unexpectedly

What it means

codex_read_response expects the codex app-server to keep its stdout open until it answers. When lines.next() returns None, the stream reached EOF — the child closed stdout (typically because the process exited or crashed) without sending the expected response. The error is thrown so callers see a clear cause instead of an empty reply.

Source

Thrown at src/skills.rs:1123

    line.push('\n');
    writer.write_all(line.as_bytes())?;
    writer.flush()?;
    Ok(())
}

fn codex_read_response(
    lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
    expected_id: u64,
    deadline: Instant,
) -> Result<serde_json::Value> {
    loop {
        if Instant::now() >= deadline {
            bail!("codex app-server response timed out");
        }
        let line = match lines.next() {
            Some(Ok(line)) => line,
            Some(Err(err)) => bail!("failed to read from codex app-server: {}", err),
            None => bail!("codex app-server closed stdout unexpectedly"),
        };
        if line.trim().is_empty() {
            continue;
        }
        let msg: serde_json::Value = serde_json::from_str(&line)
            .with_context(|| format!("invalid JSON from codex app-server: {}", line))?;
        if msg.get("id").and_then(|v| v.as_u64()) == Some(expected_id) {
            if let Some(err) = msg.get("error") {
                let message = err
                    .get("message")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown codex app-server error");
                bail!("codex app-server error: {}", message);
            }
            return Ok(msg);
        }
    }
}

View on GitHub (pinned to a747e741ae)

Solutions

  1. Run the codex app-server manually and reproduce the request to see its exit output/stderr
  2. Check the child's exit status after failure to capture why it exited (crash, panic, signal)
  3. Verify the codex binary version/protocol compatibility
  4. Ensure the child is spawned with inherited/stderr captured so crash reasons are visible

Example fix

// before
None => bail!("codex app-server closed stdout unexpectedly"),
// after: include exit status captured elsewhere
None => bail!("codex app-server closed stdout unexpectedly (exit status: {:?})", child.try_wait()),
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the child is still alive before expecting a response
if let Ok(Some(status)) = child.try_wait() {
    bail!("codex app-server already exited with {} before responding", status);
}

Try / catch

match codex_read_response(&mut lines, expected_id, deadline) {
    Ok(msg) => handle(msg),
    Err(e) if e.to_string().contains("closed stdout unexpectedly") => {
        eprintln!("codex app-server crashed; stderr was: {stderr}");
        restart_app_server()?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: The codex app-server child exits or its stdout closes before it emits the JSON response matching expected_id during reload_codex_skills_for_cwd; the process crashes on startup; the process is killed by a signal.

Common situations: Codex binary panics on the request; wrong codex executable path causing an immediate exit; version mismatch where the server exits on unknown requests; OOM kill of the child process.

Related errors


AI-assisted analysis of nikivdev/code@a747e741ae (2026-09-01). Data as JSON: /api/errors/0484181cb4f11286. Report an issue: GitHub.