herdrdev/herdr · error

ssh bootstrap stdin missing

Error message

ssh bootstrap stdin missing

What it means

sh_output spawns an ssh command with stdin piped and then writes the script to the child's stdin; if child.stdin is None (the piped stdin could not be taken, meaning spawn did not provide a stdin despite Stdio::piped()), it returns a synthetic ErrorKind::BrokenPipe 'ssh bootstrap stdin missing'. The write result is checked after wait_with_output so the ssh exit status is still observed.

Source

Thrown at src/remote/attach.rs:475

    fn base_command(&self) -> Command {
        let mut command = Command::new("ssh");
        apply_managed_ssh_options(&mut command, self.options());
        command
    }

    fn sh_output(&self, script: &str) -> io::Result<Output> {
        let mut child = self
            .command()
            .arg("/bin/sh -s")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let write_result = if let Some(mut stdin) = child.stdin.take() {
            stdin.write_all(script.as_bytes())
        } else {
            Err(io::Error::new(
                io::ErrorKind::BrokenPipe,
                "ssh bootstrap stdin missing",
            ))
        };
        let output = child.wait_with_output()?;
        write_result?;
        Ok(output)
    }

    fn user_shell_output(&self, command: &str) -> io::Result<Output> {
        self.command().arg(command).output()
    }

    fn install_herdr(&self, remote_herdr: &RemoteHerdr, source_path: &Path) -> io::Result<()> {
        let output = self.sh_output(&remote_install_prepare_script(remote_herdr))?;
        if !output.status.success() {
            return Err(command_failed("remote install preparation failed", &output));
        }

View on GitHub (pinned to f457cff4f2)

Solutions

  1. Verify the Command builder in sh_output still sets .stdin(Stdio::piped()) before .spawn()
  2. Check that the ssh binary path used for spawn exists and is executable
  3. Handle the error by surfacing the ssh stderr output (already captured) to diagnose the spawn problem
  4. Add a regression test that sh_output's spawned child always has a stdin handle

Example fix

// before
let mut child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;

// after
let mut child = cmd.stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the ssh Command has piped stdin before calling sh_output-based helpers
assert!(cmd.get_stdin().is_piped()); // or verify builder code sets .stdin(Stdio::piped())

Try / catch

match sh_output(&cmd, script) {
    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => { /* stdio setup bug; verify spawn builder and ssh presence */ return Err(e); }
    other => other?,
}

Prevention

When it happens

Trigger: Calling sh_output with ssh spawn configured .stdin(Stdio::piped()) but the returned Child has no stdin handle — practically a spawn/stdio configuration bug or an exec failure path where the pipe wasn't established.

Common situations: Refactoring the ssh spawn builder and accidentally dropping the .stdin(Stdio::piped()) call; platform-specific spawn quirks; extremely rare in practice since piped() normally guarantees a stdin handle.

Related errors


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/e7e461cbdc57e2e3. Report an issue: GitHub.