Every hook no-oped on Windows, and the smoke test was green
bin/run-hook.mjs, #3817 · found by the contributor of #3815 · one import, one catch, one CI step · 2026-08-30
The two lines
src/hooks/bin/run-hook.mjs - return await import(bundlePath); // C:\Users\...\dist\pretool.mjs: "C:" reads as a URL scheme on win32 + return await import(pathToFileURL(bundlePath).href); -} catch (err) { - // Bundle not found - likely not built yet - silentExit(); // {"continue":true,"suppressOutput":true}, exit 0, nothing else +} catch (err) { bundleLoadError = err; } +if (bundleLoadError) failBundleLoad(hookName, bundleLoadError, t1); // stderr + bundle-error row + exit 2 or 1
The comment was wrong. "Bundle not found" never reaches that catch: an absent file returns null one line earlier and stays silent, correctly. The catch only ever caught "the file exists and cannot be imported", and it reported that as a pass.
Try it: what does the runner do?
Why the Windows job never noticed
.github/workflows/windows-smoke.yml, the only functional step, before:
$payload = '{"tool_name":"Bash","session_id":"winsmoke","tool_input":{"command":"echo hello"}}'
$result = $payload | node src/hooks/bin/run-hook.mjs pretool/bash/dangerous-command-blocker
if ($LASTEXITCODE -ne 0) { throw ... }
benign command + exit-code assertion = a dead dispatcher and a live one are the same green
after: one more step, PLUGIN_ROOT=src
node tests/hooks/verdict-probes/run-probes.mjs probes.json --only bash-catastrophic-rm-deny
trip: rm -rf / -> must classify as DENY
control: ls -la -> must stay silent
- Assert the verdict, not the exit status. The probe reads the envelope the runner printed and names what it decided.
- Red before, green after. On the unfixed runner the probe reports
trip=bundle-error(the mirror added to run-probes.mjs), not the ambiguoussilent.
The control on macOS, where win32 does not exist
case (synthetic unloadable bundle: export const hooks = {) | main | this branch |
|---|---|---|
security hook: exit 2, stderr names the file, timing row verdict: bundle-error | fails | passes |
| plain hook: exit 1, same stderr and row | fails | passes |
plain hook with --rewake: exit 2 | fails | passes |
| bundle file absent: exit 0, silent envelope, no stderr, no row | passes | passes |
The fourth row is the point. A fix that made every load failure loud would also shout at the legitimate not-built case; the control pins that it stays quiet.