I let Claude Code commit directly to my repositories. I don't review the diffs. I didn't think I needed to, because I built a deterministic verify gate — a script that lints, typechecks, builds, and runs tests. If the gate goes green, the PR merges automatically.
I trusted that gate implicitly. Until I actually sat down and asked the one question that matters: If an agent pushes completely broken code right now, will this gate actually go red?
Not "is the script configured?" Not "does the file exist?"
Will it actually fail?
Turns out, for three out of my four repos, the answer was an emphatic no. The gate was returning GREEN without running a single line of code.
The line of code that lied to me
My gate script is repo-agnostic. It looks up the repository name in a policy JSON file (gates.<repoName>) and falls back to a default if it doesn't find one.
Here is the PowerShell logic driving the whole operation:
$steps = $policy.gates.$RepoName
if ($null -eq $steps) { $steps = $policy.gates.default }
if (-not $steps -or $steps.Count -eq 0) {
Write-Log "GREEN (no gate steps configured for '$RepoName')"
exit 0 # <-- Unconditional green
}
And here is my config file:
"gates": {
"app": [
"npm run lint",
"npm run test",
"<design-gate script>"
],
"default": []
}
Look at that exit code. If a repo doesn't have an explicit entry in the JSON file, it falls through to default: []. The script sees zero steps, prints GREEN, and exits with 0.
The runner sees exit code 0 and instantly merges the code. I built a system where having no tests configured is structurally identical to passing all tests.
The Audit: 4 Repos, 1 Real Gate, 0 Peace of Mind
When I audited all four repos the runner is allowed to touch, the reality was pretty grim:
-
app(The Runner itself): The only gated repo, but it's full of holes.npm run lintruns, buttsconfigexplicitly excludes therunner/directory — meaning 51 files of core executor code are never typechecked. Next.jsbuildisn't in the gate at all, so client/server boundary breaks ship freely. Vitest runs 28 test files, but the glob pattern completely misses*.test.tsxfiles. -
product(Next.js app): The most embarrassing row. It has full ESLint,tsc --noEmit,vitest(10 suites), andnext buildready to go inpackage.json. None of them were wired into the gate config. It had 0% coverage purely because of lazy config debt. -
notes(Markdown vault): Unconfigured. It doesn't need npm, but it does need a basic script to check for broken wikilinks or bad frontmatter. Instead, it was just auto-approving everything. -
portfolio(Vite site): Unconfigured and the directory wasn't even a valid Git worktree. Jobs were dying before the gate even executed, but if they had reached it, they would have passed instantly.
How to fix a fake gate
The real bug here isn't missing npm scripts. The structural flaw is that an empty gate defaults to success.
To fix this properly:
-
Make empty gates loud. If
$steps.Count -eq 0, the script must exit non-zero or return an explicitUNGATEDstatus. A missing key should break the build, not bypass it. -
Wire up the low-hanging fruit. The
productrepo was fixed with a single line inpolicy.json. -
Add actual build checks. Typechecking doesn't catch bundler or framework errors. Adding
npm run buildto the gate costs execution time, but it's the only way to catch real deployment breakers. -
Fix silent exclusion traps. Un-exclude critical core directories from
tsconfigand fix the Vitest globs before someone writes a.tsxtest that never actually gets executed.
If you run AI agents against your codebase without reading the diffs, go break a file on purpose and run your gate. If it doesn't yell at you, you don't have a safety gate — you just have a script that automatically approves bad code.
I'm Andréas — full-stack dev, CTO at a B2B SaaS, building my own agent tooling. Portfolio: https://andreas-bodin.vercel.app
Top comments (0)