Summary
Follow-up from #3503, surfaced by that PR's fresh-context verifier. Not introduced as a defect by the fix — it is an accepted trade-off of the fix, recorded here rather than widened into that PR.
plugins/guardrails/hooks/block-windows-drive-tmp.sh now evaluates its non-Windows OSTYPE gate before hook::buffer_stdin, so on Linux and macOS the hook exits 0 without reading stdin. That fix was necessary: with the gate below hook::require_jq_blocking, a jq-less non-Windows host took a fail-closed exit 2 on every Write/Edit/MultiEdit/NotebookEdit call.
The cost is that the payload is left undrained on every non-Windows tool call.
Measured
Payloads piped from cat, OSTYPE=linux-gnu, comparing 158ca9e (before the reorder) with 205fd68 (after):
| payload size |
before |
after (linux-gnu) |
after (msys) |
| 4,205 B |
writer rc 0 |
writer rc 0 |
writer rc 0 |
| 65,645 B |
writer rc 0 |
writer rc 0 |
writer rc 0 |
| 262,253 B |
writer rc 0 |
writer rc 141 (SIGPIPE) |
writer rc 0 |
| 1,048,685 B |
writer rc 0 |
writer rc 141 (SIGPIPE) |
writer rc 0 |
Above roughly 64KB–256KB the writer takes SIGPIPE. The Windows lane is unaffected.
Why it was accepted rather than fixed in #3503
- It is not proven to affect Claude Code. The measurement used
cat as the writer, which dies on SIGPIPE; that is not how the harness feeds a hook.
- The harness demonstrably tolerates an undrained hook exit already:
hook::check_enabled exits before draining whenever an operator disables any guardrails guard, and that is a supported configuration.
- Both obvious fixes are worse. A naive builtin drain loop blocks until EOF and can hang a tool call — which is precisely why
hook::buffer_stdin is a bounded idle-timeout read rather than a plain read. Draining through buffer_stdin first would put its rc-2 fail-closed exit back ahead of the host gate, reintroducing the bug the reorder fixed.
What is genuinely new
The shape (exiting without draining) was already present via check_enabled. The exposure is new: check_enabled is a kill switch that fires only when a guard is explicitly disabled, whereas this gate fires unconditionally on every non-Windows tool call — the default state for every Linux and macOS user.
Suggested direction
Decide whether guardrails wants a shared bounded-drain helper in hook-utils.sh for early-exit paths, usable by any hook that gates before reading. That is a cross-hook decision, not a single-guard one, which is why it is filed rather than patched here.
Summary
Follow-up from #3503, surfaced by that PR's fresh-context verifier. Not introduced as a defect by the fix — it is an accepted trade-off of the fix, recorded here rather than widened into that PR.
plugins/guardrails/hooks/block-windows-drive-tmp.shnow evaluates its non-WindowsOSTYPEgate beforehook::buffer_stdin, so on Linux and macOS the hook exits 0 without reading stdin. That fix was necessary: with the gate belowhook::require_jq_blocking, a jq-less non-Windows host took a fail-closedexit 2on everyWrite/Edit/MultiEdit/NotebookEditcall.The cost is that the payload is left undrained on every non-Windows tool call.
Measured
Payloads piped from
cat,OSTYPE=linux-gnu, comparing 158ca9e (before the reorder) with 205fd68 (after):Above roughly 64KB–256KB the writer takes SIGPIPE. The Windows lane is unaffected.
Why it was accepted rather than fixed in #3503
catas the writer, which dies on SIGPIPE; that is not how the harness feeds a hook.hook::check_enabledexits before draining whenever an operator disables any guardrails guard, and that is a supported configuration.hook::buffer_stdinis a bounded idle-timeout read rather than a plain read. Draining throughbuffer_stdinfirst would put its rc-2 fail-closed exit back ahead of the host gate, reintroducing the bug the reorder fixed.What is genuinely new
The shape (exiting without draining) was already present via
check_enabled. The exposure is new:check_enabledis a kill switch that fires only when a guard is explicitly disabled, whereas this gate fires unconditionally on every non-Windows tool call — the default state for every Linux and macOS user.Suggested direction
Decide whether guardrails wants a shared bounded-drain helper in
hook-utils.shfor early-exit paths, usable by any hook that gates before reading. That is a cross-hook decision, not a single-guard one, which is why it is filed rather than patched here.