#3415 · playground · 2026-08-11

Three degraded paths. One silent, one fatal.

run-hook.mjs is the single entry point every one of the 219 hooks passes through. It has three ways to degrade. Two of them warn. The third was silent, and while fixing that one a fourth problem turned up that is worse: the warning itself crashed.

The asymmetry, all inside one file

pathbeforeafter
stdin truncated at N KBthrew ReferenceErrorwarns, exit 0
truncated JSON unparsablethrew ReferenceErrorwarns, exit 0
100ms elapsed, empty {}SILENT, exit 0warns when a pipe sent 0 bytes

Why the crash was invisible

process.stderr.write(`... for hook "${name}" ...`)   // no local `name` exists

node --input-type=module -e 'typeof name'      -> "undefined"   (typeof is safe)
node --input-type=module -e '`${name}`'        -> ReferenceError

The hook name lives in hookName (:227). TypeScript resolved the bare name to the deprecated DOM global, so it reported only a soft "'name' is deprecated" hint, which reads as cosmetic. In Node ESM there is no such global, so the template literal throws. That write sits outside the try/catch beneath it, so the >512KB guard added for image pastes (#620) killed the hook with an uncaught exception rather than degrading it.

The trap in fixing it

Do not replace all four

Four sites interpolate a bare name. Only three are bugs. At :221, name is the parameter of isHookDisabled(name, overrides) and is correct, which is why the diagnostics flagged only :376, :400 and :407. A blanket replace would also have hit a site where hookName is not yet in scope (declared at :227, after it), converting a working line into a temporal-dead-zone error.

Why the timeout was not simply raised

100ms is a HANG GUARD, and PreToolUse has a <50ms budget.
A longer wait trades a silent failure for a slow one.

Discriminator instead: process.stdin.isTTY
  TTY        no payload was ever piped -> legitimate -> stay quiet
  pipe + 0B  a payload was expected and lost the race -> WARN

Positive control, run fail-then-pass

revert ONLY run-hook.mjs   exit=1  AssertionError: expected not to match /ReferenceError/
                                  ReferenceError: name is not defined
with the fix               exit=0  4 passed
full hook suite            exit=0  311 files, 7,523 passed
typecheck / builds / counts / manifests / security   all 0

The pre-fix run does not merely fail, it prints the exact ReferenceError, which is the crash proven empirically rather than argued.

A note against my own process. The verification script failed on its first run because set -e aborted at the step that was SUPPOSED to fail, before rc=$? could read it, and the abort left the fix sitting in a git stash. That is the same defect class this PR fixes, made three separate times in one session: grep -c || echo 0, then vitest | tail returning tail's status, now set -e eating an expected failure. Every intentional-failure step in the final script captures with || rc=$?.