BloopAI/vibe-kanban · error

Failed to take child stdin

Error message

Failed to take child stdin

What it means

Immediately after a successful spawn, the code takes ownership of the child's stdin handle. std::process::Child::stdin returns None only if stdin was not piped (stdin(Stdio::piped()) was not set when building the command). This error is therefore an internal invariant violation: the command was constructed without piped stdin while the stdio session requires it.

Source

Thrown at crates/embedded-ssh/src/handler.rs:98

        cmd.stdin(Stdio::piped());
        cmd.stdout(Stdio::piped());
        cmd.stderr(Stdio::piped());
        cmd.env("TERM", "xterm-256color");
        for (k, v) in env {
            cmd.env(k, v);
        }
        if let Ok(home) = std::env::var("HOME") {
            cmd.current_dir(home);
        }

        let mut child = cmd
            .spawn()
            .map_err(|e| anyhow::anyhow!("Failed to spawn stdio command: {e}"))?;

        let stdin = child
            .stdin
            .take()
            .ok_or_else(|| anyhow::anyhow!("Failed to take child stdin"))?;
        let stdout = child
            .stdout
            .take()
            .ok_or_else(|| anyhow::anyhow!("Failed to take child stdout"))?;
        let stderr = child
            .stderr
            .take()
            .ok_or_else(|| anyhow::anyhow!("Failed to take child stderr"))?;

        let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(64);
        tokio::spawn(async move {
            let mut stdin = stdin;
            while let Some(data) = writer_rx.recv().await {
                if stdin.write_all(&data).await.is_err() {
                    break;
                }
                if stdin.flush().await.is_err() {
                    break;

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Ensure the command is built with .stdin(Stdio::piped()) before spawn in spawn_stdio_session.
  2. Confirm no later code overrides stdin (e.g. Stdio::inherit/null) after it was piped.
  3. If using tokio::process, verify you're taking stdin before the child is polled to completion (handles aren't auto-closed here since it's std, but keep ordering sane).

Example fix

// before
let mut cmd = Command::new(shell);
cmd.stdin(Stdio::null());
// after
let mut cmd = Command::new(shell);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
Defensive patterns

Strategy: validation

Validate before calling

// assert construction configures piped stdio (in handler tests)
let mut cmd = Command::new(shell);
assert_piped_stdio(&mut cmd);
fn assert_piped_stdio(cmd: &mut Command) {
    cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
}

Prevention

When it happens

Trigger: spawn_stdio_session's command builder omits .stdin(Stdio::piped()) (or a code change replaced it), so child.stdin.take() returns None even though spawn succeeded.

Common situations: Practically only seen after modifying the handler's command construction or when a refactor changed which Stdio configuration is applied; end users cannot trigger it via SSH requests.

Related errors


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