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?

the bundle on disk
the hook

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

The control on macOS, where win32 does not exist

case (synthetic unloadable bundle: export const hooks = {)mainthis branch
security hook: exit 2, stderr names the file, timing row verdict: bundle-errorfailspasses
plain hook: exit 1, same stderr and rowfailspasses
plain hook with --rewake: exit 2failspasses
bundle file absent: exit 0, silent envelope, no stderr, no rowpassespasses

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.