| name | pipefail-grep-q-sigpipe-guard |
| description | Diagnose and fix false negatives caused by `grep -q` short-circuiting upstream producers under `set -euo pipefail`. |
| version | 1.0.0 |
| author | Hermes Agent |
| license | MIT |
| metadata | {"tags":["bash","pipefail","grep","sigpipe","hooks","debugging"]} |
Pipefail + grep -q SIGPIPE Guard
Use this when a Bash script running under set -euo pipefail appears to report “no match” or “no approval” even though manual inspection shows matching files or lines exist.
Symptom pattern
Common signature:
- script uses
set -euo pipefail
- check is written like
producer | grep -q pattern
- producer is something like
find, head, cat, git, or another long-running command
- the script falsely fails despite real matches
- reproducing the pipeline shows exit code
141
Example failure:
find "$repo/.planning/plan-approved" -name '*.md' -newer "$repo/.planning/STATE.md" | grep -q .
If grep -q finds the first match immediately, it exits 0 and closes the pipe. find then receives SIGPIPE. Under pipefail, the pipeline status can become non-zero, so the script treats a successful match as failure.
Root cause
grep -q is an early-exit consumer.
With pipefail, the shell reports failure if any earlier pipeline stage fails, including failure due to SIGPIPE from the consumer exiting early.
So the bug is not “grep found nothing”; the bug is “the producer was terminated after grep already succeeded.”
Safe fixes
Preferred: stop the producer at the source
find "$dir" -name '*.md' -newer "$marker" -print -quit | grep -q .
This prevents find from continuing after the first match.
Better when practical: avoid the pipeline entirely
first_match=$(find "$dir" -name '*.md' -newer "$marker" -print -quit)
[[ -n "$first_match" ]]
This is easier to reason about in hooks and enforcement scripts.
Do not assume this is the same as grep exit 1 handling
A separate pipefail pitfall is grep returning 1 for “no matches.”
This skill covers the other case: grep -q succeeded, but the producer died with SIGPIPE and poisoned the pipeline status.
Debug workflow
- Re-run the exact pipeline manually.
- Capture the pipeline exit code.
- If it is
141, suspect upstream SIGPIPE.
- Re-run the producer without
grep -q to confirm matches exist.
- Patch to
-print -quit or a non-pipeline existence test.
- Re-run the hook/script end to end.
Good target areas
- pre-commit hooks
- plan/review gates
- approval-marker existence checks
- file-discovery guards
- optional-config detection logic
- repo enforcement scripts that scan large approval-marker directories
Concrete enforcement-hook example
Observed real failure in scripts/enforcement/require-plan-approval.sh:
find "${repo_root}/.planning/plan-approved/" -name "*.md" -newer "${repo_root}/.planning/STATE.md" 2>/dev/null | grep -q .
In a repo with thousands of plan-approved/*.md markers, this produced a false blocker under set -euo pipefail:
- approval markers really existed
- manual
find showed matches
- direct pipeline reproduction returned
141
- pre-commit hook wrongly reported
NO APPROVAL
Safe patch used:
find "${repo_root}/.planning/plan-approved/" -name "*.md" -newer "${repo_root}/.planning/STATE.md" -print -quit 2>/dev/null | grep -q .
This preserves the existing structure while stopping find after the first match.
Regression-test pattern
When fixing this in repo enforcement logic, add a shell regression test that:
- creates a temp git repo
- creates
.planning/STATE.md
- creates many newer
.planning/plan-approved/*.md files (thousands, not just one)
- stages an implementation file so the gate actually runs
- executes the gate script under
--strict
- asserts exit code 0 when approval markers exist
Important: a single marker may not reproduce the SIGPIPE race reliably. Use a large marker set to force the upstream producer to still be running when grep -q exits.
Minimal reusable rule
If a pipefail script uses producer | grep -q ... for existence checks, prefer:
producer_that_can_quit_early | grep -q ..., or
- direct assignment plus
[[ -n ... ]]
Especially in enforcement hooks, do not let grep -q short-circuiting create false blocker conditions.