Skip to content

Commit f992d1b

Browse files
committed
fix(hooks): tolerate BrokenPipe when a post_tool hook ignores stdin
The CI test post_tool_captures_failure_stdout_and_stderr failed on Linux (release) while passing on macOS. Root cause: run_hook_command writes the event JSON to the hook's stdin and treated *any* write error as fatal. A verify hook that ignores stdin and exits — a bare `cargo build` / `pytest`, or any hook that reads the event from argv/env — closes its stdin read end before (or while) we write. On Linux that surfaces as BrokenPipe (EPIPE) immediately; on macOS the small event JSON fits the pipe buffer and the write completes before the child exits. So on Linux the write returned Err, run_post_tool_hooks logged it and skipped the hook, and the captured failure output never reached the model — non-deterministically losing exactly the verify-into-fix signal this feature exists to deliver. The hook simply didn't consume the event, which is not an error: swallow BrokenPipe on the stdin write and proceed to capture the hook's output and exit status. Any other write error is still fatal. The existing post_tool_captures_failure_stdout_and_stderr test (whose hook ignores stdin and exits 1) is the regression guard; it now passes on Linux.
1 parent 67d2116 commit f992d1b

1 file changed

Lines changed: 12 additions & 3 deletions

File tree

src/hooks/steering.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,18 @@ async fn run_hook_command(
154154
.take()
155155
.ok_or_else(|| "hook stdin missing".to_string())?;
156156
let body = serde_json::to_vec(stdin_json).map_err(|e| format!("hook stdin encode: {}", e))?;
157-
tokio::io::AsyncWriteExt::write_all(&mut stdin, &body)
158-
.await
159-
.map_err(|e| format!("hook stdin write: {}", e))?;
157+
// A hook that ignores its stdin and exits (a bare `cargo build` / `pytest`, or any verify hook
158+
// that reads the event from argv/env instead) closes its stdin read end before — or while — we
159+
// write. On Linux that surfaces here as `BrokenPipe`; on macOS the small event JSON usually fits
160+
// the pipe buffer and the write completes before the child exits. Treating BrokenPipe as fatal
161+
// therefore dropped the hook's captured output non-deterministically (the failure reached the
162+
// model on macOS but vanished on Linux). The hook simply didn't consume the event, which is
163+
// fine — swallow BrokenPipe and proceed to capture its output and exit status.
164+
if let Err(e) = tokio::io::AsyncWriteExt::write_all(&mut stdin, &body).await {
165+
if e.kind() != std::io::ErrorKind::BrokenPipe {
166+
return Err(format!("hook stdin write: {}", e));
167+
}
168+
}
160169
drop(stdin);
161170

162171
let stdout = child

0 commit comments

Comments
 (0)