Last month I wrote about moving this site’s CI from GitHub Actions to a pre-push git hook — same gate, no cloud bill. Tidy story, one problem: the hook was never installed.
The install lived in the prepare script in package.json: git config core.hooksPath .githooks 2>/dev/null || true. Looks harmless. But npm on Windows runs scripts through cmd.exe, where 2>/dev/null and || true are not shell syntax, they’re noise. The script failed on every npm install, the || true I’d added to be polite swallowed the evidence, and core.hooksPath never got set. Every machine I own is a Windows machine. Every push since the hook landed went to origin ungated.
It gets better. Even if the hook had run, the gate it runs was also broken on Windows. The smoke suite launches its preview server with spawn('npx', …), and Node’s spawn can’t resolve npx there — it’s npx.cmd, and without shell: true you get ENOENT. Every test then failed with a misleading “server did not come up” timeout. So the safety net was a hook that never fired, wrapping a suite that couldn’t run. Two independent failures, both silent, stacked perfectly.
Neither was hard to fix. The suite now spawns the CLI through process.execPath and the package bin — no npx, no shell, works everywhere. The prepare script routes through node, which is the one interpreter guaranteed present in an npm script on any platform. The proof for the hook fix was pushing the fix itself: the freshly wired hook fired, ran the type check and all the tests, printed “green, pushing,” and let it through. First gated push this clone ever made.
The uncomfortable part isn’t the bugs, it’s how long they sat there. A pre-push hook has no output when it isn’t installed — git just pushes. A || true means the failure path looks identical to the success path. I’d built a gate whose absence was indistinguishable from its silence, and then trusted it for weeks on the strength of a note I’d written about how tidy it was.
The lesson from the last note was “match the tooling to the size of the thing.” The lesson from this one is that the tooling has to be watched failing at least once before you believe it. If you’ve never seen your gate refuse a push, you don’t have a gate — you have a story about one.