From 1aa88463a08e9663b57cbbc009ac2bd3504c9499 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 2 Aug 2026 16:04:48 +0200 Subject: [PATCH] prefix bash-task invocations with set -o pipefail --- hive-bash-mcp/src/runner.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hive-bash-mcp/src/runner.rs b/hive-bash-mcp/src/runner.rs index 438bf487..789d4633 100644 --- a/hive-bash-mcp/src/runner.rs +++ b/hive-bash-mcp/src/runner.rs @@ -601,6 +601,16 @@ async fn run_task(mut task: TaskFile, socket: &Path) { /// mode, which disables bashisms — arrays, `[[ … ]]`, `local`, process /// substitution), invoked via `/usr/bin/env bash`. /// +/// The invocation is prefixed with `set -o pipefail` so a pipeline's exit +/// status reflects its last *failing* stage, not just its last stage — a +/// piped `cargo build 2>&1 | tail -40 && cargo test ...` chain silently +/// reports success off `tail`'s exit code otherwise, masking a real build +/// failure from every downstream `&&`/exit-code check. `-e`/`-u`/`-x` are +/// deliberately NOT forced on: unlike `pipefail` (a pure exit-status +/// reporting fix), each of those changes control flow or output volume in +/// ways plenty of ad-hoc one-liners don't expect — an opt-in a task's own +/// command string can request, not a safe default for every task. +/// /// The task is cancellable via `cancel` (set `force` first): on cancel the /// whole process group is signalled — `SIGKILL` if `force`, else `SIGINT` — /// so children of the shell die too, not just the shell. The timeout path @@ -622,7 +632,7 @@ async fn exec_cmd( cmd_builder .arg("bash") .arg("-c") - .arg(cmd) + .arg(format!("set -o pipefail; {cmd}")) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()); // SAFETY: `setpgid`/`nice` are async-signal-safe and only touch the