{"record":{"id":"084aad1e5857ee62","repo":"zed-industries/zed","slug":"ssh-process-stdout-capture-failed","errorCode":null,"errorMessage":"ssh process stdout capture failed","messagePattern":"ssh process stdout capture failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/remote/src/transport/ssh.rs","lineNumber":217,"sourceCode":"            .kill_on_drop(true)\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped())\n            .env(\"SSH_ASKPASS_REQUIRE\", \"force\")\n            .env(\"SSH_ASKPASS\", askpass_script_path)\n            .args(additional_args)\n            .args(args);\n\n        master_process.arg(format!(\"ControlPath={}\", socket_path.display()));\n\n        let process = master_process.arg(&destination).spawn()?;\n\n        Ok(MasterProcess { process })\n    }\n\n    pub async fn wait_connected(&mut self) -> Result<()> {\n        let Some(mut stdout) = self.process.stdout.take() else {\n            anyhow::bail!(\"ssh process stdout capture failed\");\n        };\n\n        let mut output = Vec::new();\n        stdout.read_to_end(&mut output).await?;\n        Ok(())\n    }\n}\n\n#[cfg(windows)]\nimpl MasterProcess {\n    const CONNECTION_ESTABLISHED_MAGIC: &str = \"ZED_SSH_CONNECTION_ESTABLISHED\";\n\n    pub fn new(\n        askpass_script_path: &std::ffi::OsStr,\n        askpass_socket_path: &std::ffi::OsStr,\n        additional_args: Vec<String>,\n        destination: &str,\n    ) -> Result<Self> {","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/zed-industries/zed/blob/f4178619acd0d47ea1f76a2025c42962c6d6638c/crates/remote/src/transport/ssh.rs#L199-L235","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Spawn the master process with piped stdout: add .stdout(Stdio::piped()) (and keep stderr piped for diagnostics) to the Command builder","Call wait_connected exactly once per MasterProcess; the Option::take design already enforces single consumption if honored","If wrapping ssh via a script or ProxyCommand, ensure it forwards the child's stdout instead of closing it"],"exampleFix":"// before\nlet process = master_process.arg(&destination).spawn()?;\n\n// after\nuse std::process::Stdio;\nlet process = master_process\n    .stdout(Stdio::piped())\n    .stderr(Stdio::piped())\n    .arg(&destination)\n    .spawn()?;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match master.wait_connected().await {\n    Err(e) if e.to_string().contains(\"stdout capture failed\") => {\n        // spawn bug: master process must be built with .stdout(Stdio::piped()); fix the builder\n        Err(e)\n    }\n    other => other,\n}","preventionTips":["Always spawn the ssh master with piped stdout and stderr","Call wait_connected exactly once per MasterProcess","Never wrap ssh in a script that closes stdout"],"tags":["ssh","process-io","stdio","invariant"],"backgroundTag":"process-stdout-unavailable","analyzedSha":"f4178619acd0d47ea1f76a2025c42962c6d6638c","analyzedAt":"2026-08-20T19:29:52.058Z","contentChangedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}