BloopAI/vibe-kanban · error · ExecutorError

Child process has no stdin

Error message

Child process has no stdin

What it means

bootstrap_acp_connection also takes the child's stdin to send ACP requests. If the child was spawned without a piped stdin, or its stdin was already taken, this error is returned. It mirrors the stdout check immediately above it in the same function.

Source

Thrown at crates/executors/src/executors/acp/harness.rs:209

        cwd: PathBuf,
        existing_session: Option<String>,
        prompt: String,
        exit_signal: Option<tokio::sync::oneshot::Sender<ExecutorExitResult>>,
        session_namespace: String,
        model: Option<String>,
        mode: Option<String>,
        approvals: Option<std::sync::Arc<dyn ExecutorApprovalService>>,
        cancel: CancellationToken,
    ) -> Result<(), ExecutorError> {
        // Take child's stdio for ACP wiring
        let orig_stdout = child.inner().stdout.take().ok_or_else(|| {
            ExecutorError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "Child process has no stdout",
            ))
        })?;
        let orig_stdin = child.inner().stdin.take().ok_or_else(|| {
            ExecutorError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "Child process has no stdin",
            ))
        })?;

        // Create a fresh stdout pipe for logs
        let writer = crate::stdout_dup::create_stdout_pipe_writer(child)?;
        let shared_writer = Arc::new(tokio::sync::Mutex::new(writer));
        let (log_tx, mut log_rx) = mpsc::unbounded_channel::<String>();

        // Spawn log -> stdout writer task
        tokio::spawn(async move {
            while let Some(line) = log_rx.recv().await {
                let mut data = line.into_bytes();
                data.push(b'\n');
                let mut w = shared_writer.lock().await;
                let _ = w.write_all(&data).await;
            }

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Ensure the child is spawned with stdin(Stdio::piped()) as well as stdout.
  2. Call bootstrap_acp_connection only once per child process.
  3. Check that no wrapper script or prior harness consumed the agent's stdin.
  4. Review the executor spawn path for the ACP configuration to confirm both pipes are requested.

Example fix

// before
let child = cmd.spawn()?; // stdin defaults to inherit
// after
cmd.stdin(Stdio::piped());
let child = cmd.spawn()?;
Defensive patterns

Strategy: type-guard

Type guard

fn has_piped_stdio(child: &Child) -> bool {
    child.stdin.is_some() && child.stdout.is_some()
}
if !has_piped_stdio(&child) {
    anyhow::bail!("child must be spawned with piped stdin/stdout");
}

Try / catch

match bootstrap_acp_connection(...).await {
    Err(ExecutorError::Io(e)) if e.kind() == ErrorKind::NotFound
        && e.to_string().contains("no stdin") => {
        // respawn the agent with Stdio::piped() stdin
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling bootstrap_acp_connection on a child spawned without Stdio::piped() for stdin, or a second bootstrap call after stdin was consumed by the first.

Common situations: Spawning the agent with stdin inherited from an interactive terminal or set to null; double initialization of the ACP harness; wrapper scripts consuming stdin before the protocol starts.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/b3b043fd0ecc199c. Report an issue: GitHub.