herdrdev/herdr · error

ssh bridge stdin missing

Error message

ssh bridge stdin missing

What it means

The SSH bridge child was spawned with Stdio::piped() for stdin, but child.stdin was None when taken. This BrokenPipe error indicates the child's stdin handle was already consumed or unavailable, so bridging cannot upload data to ssh.

Source

Thrown at src/remote/attach.rs:1841

    _bridge_stop: &Arc<AtomicBool>,
) -> io::Result<()> {
    let mut command = Command::new("ssh");
    apply_managed_ssh_options(&mut command, ssh_options);
    command
        .arg("-T")
        .arg(target)
        .arg(remote_bridge_command(remote_herdr, session_name))
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit());

    let mut child = command
        .spawn()
        .map_err(|err| io::Error::new(err.kind(), format!("failed to start ssh bridge: {err}")))?;
    let mut child_stdin = child
        .stdin
        .take()
        .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "ssh bridge stdin missing"))?;
    let mut child_stdout = child
        .stdout
        .take()
        .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "ssh bridge stdout missing"))?;
    let mut stream_to_child = stream.try_clone()?;
    let mut child_to_stream = stream;

    let upload = thread::spawn(move || {
        let _ = copy_flush(&mut stream_to_child, &mut child_stdin);
    });
    let download = thread::spawn(move || {
        let _ = copy_flush(&mut child_stdout, &mut child_to_stream);
        let _ = crate::ipc::shutdown_local_stream_write(&child_to_stream);
    });

    let status = child.wait()?;
    let _ = upload.join();
    let _ = download.join();

View on GitHub (pinned to f457cff4f2)

Solutions

  1. Retry the attach — transient pipe setup failures can occur under fd exhaustion
  2. Check for fd/resource limits (ulimit -n) that may cause pipe creation to silently degrade
  3. Report as a bug if it reproduces consistently, since spawn was configured with piped stdin
Defensive patterns

Strategy: retry

Try / catch

match bridge() {
    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe && e.to_string().contains("stdin missing") => {
        bridge() // one retry; pipe setup races are transient
    }
    r => r,
}

Prevention

When it happens

Trigger: Immediately after a successful spawn, taking .stdin on the Child returns None — typically only possible if the handle was taken twice or the Child was constructed without piped stdin; in practice this guards against internal API misuse or race in bridge setup.

Common situations: Rare internal invariant violation; more commonly surfaced when code changes reuse the Child or when platform runtime quirks drop the pipe handle.

Related errors


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