DEV Community

Akash Khanna
Akash Khanna

Posted on

I Had 16 AI Agents in my repo including security auditor and reviewer. Not One of Them Existed.

My project instructions said this, in writing, for weeks:

Migrations are enforced by the auto-invoked migration-reviewer subagent.

They weren't. There was no migration-reviewer. There was a file called migration-reviewer.md, and fifteen more like it, sitting in a folder one level too deep.

Here's the part that makes it worse: Claude Code told me to put them there. I asked where agent files should live, it said inside the repo, I moved them in. Weeks later, a different session said they should be outside. I had no idea why they never triggered — I just knew they didn't. Turns out Claude Code looks for agents by walking up from where you launched it. It never looks down. So it never found them.

Sixteen AI reviewers. Zero of them ever ran. No error. No warning. No log line. Just weeks of quiet.

That's the bug class this whole post is about, and it's the one that should scare you: things that fail open. A crash is loud. A failing test is loud. But a safety net that silently isn't there looks exactly like a safety net that's working.

I only found out because I went looking.

It gets worse when you look closely

Once I actually read those agent files, the four newest ones each had their own private disaster:

reviewer.md had no frontmatter at all. It was workflow prose — copy-pasted notes — saved into the agents folder. An agent file needs a name and a description to load. This had neither. It could never load, ever.

And my instructions told the AI to "invoke the reviewer subagent until it returns APPROVED." My pre-commit review gate was a reference to nothing. A door with no room behind it.

model: claude-opus-4.8 — the real ID is claude-opus-4-8. A dot instead of a hyphen. Nothing errors on a bad model ID. It just quietly falls back to something else.

Two agents had byte-identical descriptions. The AI picks an agent by reading its description. Two identical descriptions means it has no way to choose. It's a coin flip wearing a lab coat.

Every single one of these fails silently. That's the pattern. That's the whole post.

"All tests pass"

Here's the one that actually costs you money.

Your AI says "All tests pass." Reasonable! It ran npm test. It's not lying.

Except:

The test was killed. Out of memory. The process died with a non-zero exit code and printed the word Killed. Not FAILED, not 3 failing — just Killed. The checker was scanning the output text for failure words, found none, and called it green. The exit code — the actual, unambiguous, machine-readable did-this-work signal — was sitting right there in the transcript, and nothing was reading it.

The green was stale. The tests ran. They passed. Then the AI edited the source file. Then it said "all tests pass." Technically true, completely worthless — that green refers to code that no longer exists.

The green was one file. npm test -- --grep billing runs one slice of your suite and passes. It shows up in the history as "tests ran." Claim: "all tests pass." Reality: you tested 4% of your code.

None of these are the AI lying. Every one is the AI telling the truth about a thing that doesn't mean what you think it means.

The email that shipped garbage every morning

This one happened during EraPin development — a classroom geography and history game I'm building for teachers.

One bad file write re-encoded a source file — read it as one text format, saved it as another. 756 characters silently mangled. Every arrow, every checkmark, every dash in that file turned into gibberish.

The code still ran. It ran perfectly. It just faithfully sent that gibberish to every subscriber, every morning, for days.

Nothing caught it. Not the tests — the tests don't read the email. Not the linter — it's valid code. Not code review — you skim a 374-line diff and your eye slides right over it.

Here's the kicker: 365 of those 374 changed lines carried the corruption, and the previous commit was completely clean. A dumb, thirty-line check running at commit time would have caught it instantly. Nobody had written one, because who writes an encoding checker? You do, right after this happens to you.

The fix that changed nothing

My favourite, because it's the most human.

I "fixed" the environment label on some emails. Prefixed the subject lines. Looked done. Was done, in the diff.

Those two email functions are never called. The scheduled job runs with a flag that skips them entirely and sends its own summary instead. My fix touched code that does not execute. It would have shipped, looked complete, and changed absolutely nothing about the email you actually receive.

A diff can't see this. There's no stub, no TODO, no missing file. Every line is real. It's just dead.

And here's the thing I got wrong for a while: I assumed catching this needed AI. It doesn't. It needs a coverage run. "Did the lines I changed actually execute when I ran the thing?" is a question with a boring, deterministic, no-AI-required answer. Run it. Read the counter. My changed lines would have shown as never executed.

That reframe matters more than any single bug in this post.

Now the uncomfortable part

I build Groundtruth — a tool that catches exactly this stuff. The false "done," the silent no-op, the fake green. Deterministic, no AI in the loop.

This week, adversarial review found 13 real bugs in it. Every single one had already passed my own test suite and my own manual checks.

The highlights, and they're humbling:

  • A check I wrote to catch "tests that don't actually test anything" had four false positives in its first version. It flagged real, working tests.
  • I fixed a false positive and introduced a false negative — the tool went quiet on real problems. I did this three separate times, in the same place, the same way.
  • I wrote a classifier to fix a false positive, and it re-opened the exact false positive it was written to fix.
  • The check that finds "agents that can't load" couldn't see uncommitted files — meaning it would have missed all four of my broken new agents. The most likely place to find a broken thing is the thing you just wrote and haven't committed.
  • At one point a false positive fired on my own sentence describing that false positive. The tool flagged the warning about the tool.

If you take one thing from this: your own tests are not a review. They only check what you already thought of. That's the whole limitation, right there in one sentence.

The finding that surprised me most

Everyone treats false positives as annoying. Noise. Alert fatigue. A UX problem.

They're worse than that. False positives hide false negatives.

One of my checks was firing constantly — roughly 40 times in a single session, almost all wrong. Pure noise. So I scoped it properly and the noise stopped.

And the moment the noise stopped, a real hole became visible underneath it: run your test suite, watch it fail red, then run one trivial passing test, then claim "all tests pass" — and the tool went completely silent. A genuine way to launder a broken build past the gate. It had been there the whole time. The false positive was accidentally covering it, so nobody ever saw the silence for what it was.

Noise isn't just noise. Noise is camouflage.

The mental model I'd actually keep

If you're building anything that checks AI output, this is the part worth stealing. There are three ways to check something, and most people skip straight from the first to the third:

Tier 1 — Look at what you already have. The diff. The command history. The exit code. Free, instant, provable. Most people never do this properly — I certainly wasn't reading exit codes.

Tier 2 — Run something new. Coverage. Mutation testing. Did the code I changed actually execute? No AI required — it just costs you one more run. This is the tier almost everyone forgets exists, and it's where the "my fix changed nothing" bug lives.

Tier 3 — Ask an AI. Last resort. Slow, expensive, and it misses things.

And the twist that took me a week to see: under a rule of "a false alarm is fatal," tier 2 — the deterministic, no-AI tier — is the dangerous one. Mutation testing over-reports. It cries wolf. "Deterministic" was never the safety property. "Provably right, or shut up" is.

What it still can't do (being honest)

  • It can't see runtime. I wrote 60 database rows this week. Invisible. It referees the diff, not the world.
  • It can't tell you a better approach existed. "You picked a worse way that happens to pass" has no ground truth to check against — you never wrote down what "better" meant. Nothing can check that. Not a tool, not an AI. Only you.
  • Pointed at its own development, ~75% of its findings were false positives. Because when your work product is literally the words the sensor looks for, everything trips.

The one line to keep

A reader left this in my last comment section and it's been the design constraint ever since:

"A checker that false-fires gets ignored within a week, and then it catches nothing."

Sixteen agents that never ran. A review gate pointing at nothing. A green checkmark from a killed process. An email full of garbage that every test approved of.

None of it errored. None of it alerted. All of it looked exactly like success.

That's the actual job: not making the AI smarter. Making the silence impossible.

Everything above is real, from two days of sessions. Five releases, a test suite that went from ~500 checks to 702, and 13 bugs found in the bug-finder — by review, not by me.


I write these while building Groundtruth and EraPin. The three-tier model came from a dev.to comment thread where two engineers refined the architecture better than I had — and I credited them, because the tool's whole point is that honesty scales and bluffing doesn't.

Top comments (0)