Skip to content

fix(hook): bound stdin read so a never-closing stdin can't wedge the hook - #172

Merged
Raftersecurity merged 1 commit into
mainfrom
sable-hook-stdin-bound
Jun 17, 2026
Merged

fix(hook): bound stdin read so a never-closing stdin can't wedge the hook#172
Raftersecurity merged 1 commit into
mainfrom
sable-hook-stdin-bound

Conversation

@Rome-1

@Rome-1 Rome-1 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Problem

rafter hook pretool / hook posttool read stdin with a 5s timeout, but on Node that timeout only bounded output latency, not process exit. A piped stdin with no EOF stays in flowing mode and keeps the event loop alive — so after emitting its decision at 5s, the hook process hung indefinitely.

Reproduced (before fix):

$ sleep 30 | rafter hook pretool --format claude
{"hookSpecificOutput":{...,"permissionDecision":"allow",...}}   # printed at 5s
# ...then hangs forever — outer guard had to SIGKILL it (rc=124)

Any host that opens the hook's stdin but never writes/closes it wedges a zombie node per tool call. (Reported via hq-ryfyt from codegen_security_arena/crew/turing.)

Fix

  • Node: on the timeout/end/error paths, remove listeners and process.stdin.pause() so the event loop drains and the process exits (~5s, fail-open). Verified: never-closing stdin now exits at ~5.5s default / ~0.9s with a 300ms override.
  • Python: already exited (daemon reader thread doesn't block interpreter exit) — behavior unchanged; added a clarifying comment.
  • Adds RAFTER_HOOK_STDIN_TIMEOUT_MS (ms, default 5000) as an operator safety valve and to keep tests fast. Both implementations parse it identically: finite and > 0, else fall back to default — so Infinity / nan / 0 / negative / garbage can never re-create the unbounded wait.

Security review (required gate)

Ran rafter-code-review (CWE Top-25 walk) on the diff. One finding, fixed in this PR:

Parity/fail-safe gap: Node rejects non-finite via Number.isFinite, but Python's float("inf") > 0 is Truejoin(timeout=inf) would reintroduce the exact hang. Guarded Python with math.isfinite to match Node.

Local secrets scan clean on all changed files. rafter run (remote SAST) not executed — no RAFTER_API_KEY in this environment; recommend CI runs it.

Tests

  • Node (hook-stdin-timeout.test.ts): spawns the real hook with a never-closing stdin, asserts bounded exit + fail-open decision (pretool & posttool).
  • Python (test_hook_stdin_timeout.py): asserts the bound returns promptly on a blocking stdin, and env parsing rejects non-finite / non-positive / garbage.
  • All existing hook suites pass. (Note: hook-integration.test.ts > blocks rm -rf / fails on main too — pre-existing, unrelated to this diff; filed separately.)

🤖 Generated with Claude Code

…hook

`rafter hook pretool/posttool` read stdin with a 5s timeout, but on Node the
timeout only bounded OUTPUT latency, not process exit: a piped stdin with no
EOF stays in flowing mode and keeps the event loop alive, so after emitting its
decision at 5s the process hung indefinitely. Reproduced via `sleep 30 | rafter
hook pretool` (outer guard had to SIGKILL it). The fix pauses stdin and removes
listeners on the timeout/end/error paths so the loop drains and the process
exits (~5s, fail-open). Python already exited (daemon reader thread) — unchanged
in behavior.

Adds RAFTER_HOOK_STDIN_TIMEOUT_MS (ms, default 5000) as an operator safety
valve and to keep the regression tests fast. Both implementations parse it
identically: finite and > 0, else fall back to the default — so Infinity / nan
/ 0 / negative / garbage can never re-create the unbounded wait (Python guarded
with math.isfinite to match Node's Number.isFinite, preventing join(timeout=inf)).

Regression tests spawn the real hook with a never-closing stdin and assert
bounded exit + fail-open decision (Node), and assert the bound + env parsing
including non-finite rejection (Python). CLI_SPEC.md documents the bounded read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Raftersecurity
Raftersecurity merged commit 8070a99 into main Jun 17, 2026
@Raftersecurity
Raftersecurity deleted the sable-hook-stdin-bound branch June 17, 2026 21:57
@Rome-1 Rome-1 mentioned this pull request Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants