zed-industries/zed · error

ssh process stdout capture failed

Error message

ssh process stdout capture failed

What it means

On the Unix path, MasterProcess::wait_connected takes the ssh master process's piped stdout to detect connection establishment; if the Child's stdout is None — spawned without Stdio::piped, or already consumed by an earlier take() — this bail fires. It is an internal invariant about how the ssh master was spawned rather than a network condition.

Source

Thrown at crates/remote/src/transport/ssh.rs:217

            .kill_on_drop(true)
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .env("SSH_ASKPASS_REQUIRE", "force")
            .env("SSH_ASKPASS", askpass_script_path)
            .args(additional_args)
            .args(args);

        master_process.arg(format!("ControlPath={}", socket_path.display()));

        let process = master_process.arg(&destination).spawn()?;

        Ok(MasterProcess { process })
    }

    pub async fn wait_connected(&mut self) -> Result<()> {
        let Some(mut stdout) = self.process.stdout.take() else {
            anyhow::bail!("ssh process stdout capture failed");
        };

        let mut output = Vec::new();
        stdout.read_to_end(&mut output).await?;
        Ok(())
    }
}

#[cfg(windows)]
impl MasterProcess {
    const CONNECTION_ESTABLISHED_MAGIC: &str = "ZED_SSH_CONNECTION_ESTABLISHED";

    pub fn new(
        askpass_script_path: &std::ffi::OsStr,
        askpass_socket_path: &std::ffi::OsStr,
        additional_args: Vec<String>,
        destination: &str,
    ) -> Result<Self> {

View on GitHub (pinned to f4178619ac)

Solutions

  1. Spawn the master process with piped stdout: add .stdout(Stdio::piped()) (and keep stderr piped for diagnostics) to the Command builder
  2. Call wait_connected exactly once per MasterProcess; the Option::take design already enforces single consumption if honored
  3. If wrapping ssh via a script or ProxyCommand, ensure it forwards the child's stdout instead of closing it

Example fix

// before
let process = master_process.arg(&destination).spawn()?;

// after
use std::process::Stdio;
let process = master_process
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .arg(&destination)
    .spawn()?;
Defensive patterns

Strategy: try-catch

Try / catch

match master.wait_connected().await {
    Err(e) if e.to_string().contains("stdout capture failed") => {
        // spawn bug: master process must be built with .stdout(Stdio::piped()); fix the builder
        Err(e)
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling wait_connected twice on the same MasterProcess (the first take() emptied the Option), or constructing the master ssh command without .stdout(Stdio::piped()) in the builder.

Common situations: Local Zed source modifications that drop the piped stdout from the master_process builder; two tasks racing to call wait_connected; an ssh wrapper or ProxyCommand that closes or redirects stdout.

Related errors


AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20). Data as JSON: /api/errors/084aad1e5857ee62. Report an issue: GitHub.