herdrdev/herdr · critical

failed to start ssh bridge: {err}

Error message

failed to start ssh bridge: {err}

What it means

Spawning the SSH bridge child process (with piped stdin/stdout and inherited stderr) failed; the OS spawn error is wrapped preserving its kind. This happens before any bridging starts, meaning the ssh command itself could not be launched.

Source

Thrown at src/remote/attach.rs:1837

    target: &str,
    remote_herdr: &RemoteHerdr,
    session_name: &str,
    ssh_options: Option<&ManagedSshOptions>,
    _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);
    });

View on GitHub (pinned to f457cff4f2)

Solutions

  1. Verify ssh is installed: ssh -V
  2. Install openssh-client on the local machine
  3. Fix PATH for non-interactive environments (set it in the shell profile herdr inherits)
  4. Retry when the machine is not under resource pressure

Example fix

# before: no ssh client
# after
apt-get update && apt-get install -y openssh-client
Defensive patterns

Strategy: validation

Validate before calling

fn ssh_available() -> bool {
    std::process::Command::new("ssh")
        .arg("-V")
        .output()
        .is_ok()
}

Try / catch

match attach() {
    Err(e) if e.to_string().contains("failed to start ssh bridge") => {
        eprintln!("check that ssh is installed and on PATH");
        Err(e)
    }
    r => r,
}

Prevention

When it happens

Trigger: The ssh executable is missing from PATH, not executable, argument list issues, or process/resource limits preventing spawn when establishing the local-to-remote stdio bridge.

Common situations: Containers or minimal hosts without openssh-client, PATH stripped in non-interactive shells, fork failures under memory pressure.

Related errors


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