diff --git a/crates/pi-shell/src/fd.rs b/crates/pi-shell/src/fd.rs index 93ffbbced..021b8856f 100644 --- a/crates/pi-shell/src/fd.rs +++ b/crates/pi-shell/src/fd.rs @@ -647,6 +647,10 @@ fn run_fd_sync( 0 } }, + // A closed downstream reader (`fd … | head`) surfaces as BrokenPipe on + // stdout writes. Real fd dies silently from SIGPIPE; mirror that with + // exit 141 (128+SIGPIPE) and no diagnostic. + Err(err) if err.kind() == io::ErrorKind::BrokenPipe => 141, Err(err) => { let _ = writeln!(stderr, "fd: {err}"); 2 @@ -1650,7 +1654,7 @@ mod tests { use brush_core::openfiles::OpenFile; use clap::Parser; - use super::{FdCli, cancel_heartbeat, search}; + use super::{FdCli, cancel_heartbeat, run_fd_sync, search}; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -1803,4 +1807,28 @@ mod tests { assert!(err.is_empty(), "stderr should stay clean on success: {err:?}"); let _ = fs::remove_dir_all(&tree); } + + #[test] + fn broken_pipe_on_stdout_is_silent_and_exits_141() { + // Regression: `fd … | head` printed "fd: Broken pipe (os error 32)" + // when the downstream builtin closed the read end early. Real fd dies + // silently from SIGPIPE; the builtin must map BrokenPipe to exit 141 + // with no stderr diagnostic. + let tree = seeded_tree("epipe"); + let (reader, writer) = std::io::pipe().expect("pipe"); + drop(reader); // downstream reader (e.g. `head`) already exited + let mut stdout = OpenFile::from(writer); + let (stderr_capture, stderr_file) = capture_file("stderr-epipe"); + let mut stderr = OpenFile::from(stderr_file); + let cancelled = AtomicBool::new(false); + let argv = ["fd", "haystack", tree.to_str().expect("utf8 path")] + .map(std::ffi::OsString::from) + .to_vec(); + let code = run_fd_sync(argv, tree.clone(), &mut stdout, &mut stderr, &cancelled); + drop(stderr); + assert_eq!(code, 141, "BrokenPipe must map to 128+SIGPIPE"); + let err = read_all(&stderr_capture); + assert!(err.is_empty(), "stderr must stay clean on a broken pipe: {err:?}"); + let _ = fs::remove_dir_all(&tree); + } } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1c88f0ca1..919f78575 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed - Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)). +- Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd. ## [16.5.2] - 2026-07-14