<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akash Khanna</title>
    <description>The latest articles on DEV Community by Akash Khanna (@akahkhanna).</description>
    <link>https://dev.to/akahkhanna</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4012589%2F2c3d32e0-bd94-45e5-8dc8-f71db7c27c33.png</url>
      <title>DEV Community: Akash Khanna</title>
      <link>https://dev.to/akahkhanna</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akahkhanna"/>
    <language>en</language>
    <item>
      <title>I Tried to Catch My AI Coding Assistant Lying. Here's What Finally Worked.</title>
      <dc:creator>Akash Khanna</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:39:34 +0000</pubDate>
      <link>https://dev.to/akahkhanna/i-tried-to-catch-my-ai-coding-assistant-lying-heres-what-finally-worked-4bg0</link>
      <guid>https://dev.to/akahkhanna/i-tried-to-catch-my-ai-coding-assistant-lying-heres-what-finally-worked-4bg0</guid>
      <description>&lt;p&gt;If you've ever used an AI coding assistant, you've probably seen it say something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Done! I've created the file, updated the config, and all tests pass. ✅"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes that's true. Sometimes it isn't. The file is half-finished, the "test" never actually ran, and you only find out later when things break. The AI isn't being evil — it just sometimes describes what it &lt;em&gt;meant&lt;/em&gt; to do instead of what it &lt;em&gt;actually&lt;/em&gt; did.&lt;/p&gt;

&lt;p&gt;I built a small free tool called &lt;strong&gt;GroundTruth&lt;/strong&gt; to catch this. My first version failed for months. The rewrite finally works, and the lesson behind it is useful even if you never touch my tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: Read what the AI says, and fact-check it
&lt;/h2&gt;

&lt;p&gt;My first idea was simple. When the AI finishes a task, a little program automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the AI's final message ("I created the login page and tests pass")&lt;/li&gt;
&lt;li&gt;Looks at what actually changed in the project (your version control system, git, keeps a perfect record — the AI can't fake that)&lt;/li&gt;
&lt;li&gt;Compares the two and warns you about any gaps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 and 3 worked great. Step 1 was the disaster.&lt;/p&gt;

&lt;p&gt;To find the promises inside the AI's message, I wrote text-matching patterns — rules like &lt;em&gt;"if the message contains 'tests pass', treat that as a claim."&lt;/em&gt; Sounds reasonable. Except:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The tests &lt;strong&gt;should&lt;/strong&gt; pass" isn't a claim — it's a hope.&lt;/li&gt;
&lt;li&gt;"I did &lt;strong&gt;not&lt;/strong&gt; say the tests pass" contains the words "tests pass."&lt;/li&gt;
&lt;li&gt;"The file was updated" dodges every pattern written for "I updated the file."&lt;/li&gt;
&lt;li&gt;And there are infinite more ways to phrase anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up with over 50 patterns, each fixing the last one's mistakes, and it &lt;em&gt;still&lt;/em&gt; got something wrong every single session. Eventually I accepted why: &lt;strong&gt;human language has unlimited ways to say the same thing. No list of patterns can ever cover them all.&lt;/strong&gt; I wasn't losing because I was bad at it. The game itself was unwinnable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: Stop reading essays. Ask for a form.
&lt;/h2&gt;

&lt;p&gt;Think about how the real world handles this. Customs doesn't read your travel diary to figure out what's in your suitcase — you fill in a declaration form, and &lt;em&gt;then&lt;/em&gt; they can compare the form to the suitcase.&lt;/p&gt;

&lt;p&gt;So version 2 flips the whole design. The AI must now end every task with a short, fixed-format summary — a form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add a login page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/login.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/app.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests_pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm test"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just eight allowed entry types (created a file, modified a file, ran tests, and so on). Then my tool does two very simple checks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 1 — everything on the form must be real.&lt;/strong&gt; Claimed you created &lt;code&gt;login.js&lt;/code&gt;? It must actually appear in git's record of changes. Claimed the tests passed? That exact command must have actually run during the session — and finished successfully. (The session log is kept by the coding tool itself, so the AI can't rewrite history.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 2 — everything real must be on the form.&lt;/strong&gt; If the AI changed a file it didn't declare, that's flagged too: &lt;em&gt;"undeclared change."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those two checks trap it from both sides. Invent work to look productive → caught. Hide work to cover up sloppiness → caught. Skip the form entirely → the task isn't allowed to finish; the tool hands the format back and the AI fills it in properly. Lie in the chat message? Nobody cares — the chat message isn't what gets checked anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that surprised me
&lt;/h2&gt;

&lt;p&gt;Comparing a small form to a git record is &lt;em&gt;easy&lt;/em&gt;. It's exact matching — no guessing, no interpretation, no AI judging another AI. When I deleted all the language-guessing code, the tool got about 700 lines &lt;strong&gt;smaller&lt;/strong&gt; and catches &lt;strong&gt;more&lt;/strong&gt; than it ever did.&lt;/p&gt;

&lt;p&gt;A few gotchas still took real work. For example: how do you know "npm test" truly ran? The command &lt;code&gt;echo "npm test passed"&lt;/code&gt; just &lt;em&gt;prints&lt;/em&gt; those words and reports success. &lt;code&gt;npm test || true&lt;/code&gt; forces a success signal even when tests fail. A test run from &lt;em&gt;before&lt;/em&gt; the latest code change proves nothing about the change. Each of those tricks needed its own small, precise check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway (even if you never use my tool)
&lt;/h2&gt;

&lt;p&gt;If you're building anything that needs to trust what an AI tells you — don't try to interpret its free-form writing. You'll be patching misreadings forever.&lt;/p&gt;

&lt;p&gt;Instead: &lt;strong&gt;give it a small form to fill in, make the form mandatory, and check the form against facts the AI can't fake.&lt;/strong&gt; Turning "understand a sentence" into "compare two lists" changes an impossible problem into a boring one. Boring is good.&lt;/p&gt;

&lt;p&gt;GroundTruth is free, open source (MIT), works with Claude Code, and never sends your code anywhere — no AI calls, no network, no API key.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;https://github.com/akahkhanna/groundtruth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions welcome — especially "would this catch X?" ones. Those are the fun ones.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Had 16 AI Agents in my repo including security auditor and reviewer. Not One of Them Existed.</title>
      <dc:creator>Akash Khanna</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:59:35 +0000</pubDate>
      <link>https://dev.to/akahkhanna/i-had-16-ai-reviewers-guarding-my-code-not-one-of-them-existed-ic4</link>
      <guid>https://dev.to/akahkhanna/i-had-16-ai-reviewers-guarding-my-code-not-one-of-them-existed-ic4</guid>
      <description>&lt;p&gt;My project instructions said this, in writing, for weeks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Migrations are enforced by the auto-invoked migration-reviewer subagent.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Here's the part that makes it worse: &lt;strong&gt;Claude Code told me to put them there.&lt;/strong&gt; 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 &lt;em&gt;up&lt;/em&gt; from where you launched it. It never looks &lt;em&gt;down&lt;/em&gt;. So it never found them.&lt;/p&gt;

&lt;p&gt;Sixteen AI reviewers. Zero of them ever ran. No error. No warning. No log line. Just weeks of quiet.&lt;/p&gt;

&lt;p&gt;That's the bug class this whole post is about, and it's the one that should scare you: &lt;strong&gt;things that fail open.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;I only found out because I went looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  It gets worse when you look closely
&lt;/h2&gt;

&lt;p&gt;Once I actually read those agent files, the four newest ones each had their own private disaster:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Two agents had byte-identical descriptions.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;Every single one of these fails silently. That's the pattern. That's the whole post.&lt;/p&gt;

&lt;h2&gt;
  
  
  "All tests pass"
&lt;/h2&gt;

&lt;p&gt;Here's the one that actually costs you money.&lt;/p&gt;

&lt;p&gt;Your AI says "All tests pass." Reasonable! It ran &lt;code&gt;npm test&lt;/code&gt;. It's not lying.&lt;/p&gt;

&lt;p&gt;Except:&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;The green was one file.&lt;/strong&gt; &lt;code&gt;npm test -- --grep billing&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The email that shipped garbage every morning
&lt;/h2&gt;

&lt;p&gt;This one happened during &lt;a href="https://erapin.com" rel="noopener noreferrer"&gt;EraPin&lt;/a&gt; development — a classroom geography and history game I'm building for teachers.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The code still ran. It ran perfectly. It just faithfully sent that gibberish to every subscriber, every morning, for days.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that changed nothing
&lt;/h2&gt;

&lt;p&gt;My favourite, because it's the most human.&lt;/p&gt;

&lt;p&gt;I "fixed" the environment label on some emails. Prefixed the subject lines. Looked done. Was done, in the diff.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A diff can't see this. There's no stub, no &lt;code&gt;TODO&lt;/code&gt;, no missing file. Every line is real. It's just dead.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That reframe matters more than any single bug in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the uncomfortable part
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;Groundtruth&lt;/a&gt; — a tool that catches exactly this stuff. The false "done," the silent no-op, the fake green. Deterministic, no AI in the loop.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The highlights, and they're humbling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;I wrote a classifier to fix a false positive, and it re-opened the exact false positive it was written to fix.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;At one point a false positive fired on my own sentence describing that false positive. The tool flagged the warning about the tool.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  The finding that surprised me most
&lt;/h2&gt;

&lt;p&gt;Everyone treats false positives as annoying. Noise. Alert fatigue. A UX problem.&lt;/p&gt;

&lt;p&gt;They're worse than that. &lt;strong&gt;False positives hide false negatives.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Noise isn't just noise. Noise is camouflage.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model I'd actually keep
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Tier 2 — Run something new.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — Ask an AI.&lt;/strong&gt; Last resort. Slow, expensive, and it misses things.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What it still can't do (being honest)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It can't see runtime.&lt;/strong&gt; I wrote 60 database rows this week. Invisible. It referees the diff, not the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't tell you a better approach existed.&lt;/strong&gt; "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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointed at its own development, ~75% of its findings were false positives.&lt;/strong&gt; Because when your work product is literally the words the sensor looks for, everything trips.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one line to keep
&lt;/h2&gt;

&lt;p&gt;A reader left this in my last comment section and it's been the design constraint ever since:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A checker that false-fires gets ignored within a week, and then it catches nothing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;None of it errored. None of it alerted. All of it looked exactly like success.&lt;/p&gt;

&lt;p&gt;That's the actual job: not making the AI smarter. &lt;strong&gt;Making the silence impossible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these while building &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;Groundtruth&lt;/a&gt; and &lt;a href="https://erapin.com" rel="noopener noreferrer"&gt;EraPin&lt;/a&gt;. The three-tier model came from a &lt;a href="https://dev.to/akahkhanna/"&gt;dev.to comment thread&lt;/a&gt; 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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The "4 layers to stop Claude lying" setup is a duct-tape stack. Here's what a single hook does instead.</title>
      <dc:creator>Akash Khanna</dc:creator>
      <pubDate>Sun, 12 Jul 2026 10:03:32 +0000</pubDate>
      <link>https://dev.to/akahkhanna/the-4-layers-to-stop-claude-lying-setup-is-a-duct-tape-stack-heres-what-a-single-hook-does-1m3c</link>
      <guid>https://dev.to/akahkhanna/the-4-layers-to-stop-claude-lying-setup-is-a-duct-tape-stack-heres-what-a-single-hook-does-1m3c</guid>
      <description>&lt;p&gt;A viral post made the rounds recently: "How to Make Claude Code Stop Making Stuff Up When It Doesn't Know." It described a 4-layer setup — honesty rules in CLAUDE.md, verification protocols, linter hooks, and a fact-checker subagent — to catch Claude fabricating functions, faking test results, and confidently delivering nonsense.&lt;/p&gt;

&lt;p&gt;The post was good. The problem it named is real. But the solution is a duct-tape stack, and every layer has a gap the next one is supposed to cover. Four moving parts, all configured by the user, all depending on the agent's willingness to follow the rules that are supposed to catch it breaking the rules.&lt;/p&gt;

&lt;p&gt;I built a single hook that replaces all four. Here's why the layers don't hold, and what does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 4 layers actually are
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Honesty rules in CLAUDE.md.&lt;/strong&gt; Tell Claude to admit uncertainty, cite file:line when referencing code, and ask before adding dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Verification protocol.&lt;/strong&gt; More CLAUDE.md instructions forcing Claude to check files exist before importing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Linter/type-checker hooks.&lt;/strong&gt; A PostToolUse hook that runs &lt;code&gt;tsc&lt;/code&gt; or a linter after every file write. If Claude invented an import, the checker fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4: Fact-checker subagent.&lt;/strong&gt; A second agent invoked before commits whose job is to verify claims made by the first.&lt;/p&gt;

&lt;p&gt;Each layer is reasonable in isolation. Together, they share a structural problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structural problem: layers 1, 2, and 4 ask the agent to police itself
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md rules (layers 1 and 2) work by telling Claude to behave differently. But Claude's fabrication problem exists &lt;em&gt;because&lt;/em&gt; it's optimised to look helpful, and admitting uncertainty feels unhelpful. You're asking the same optimisation that causes the lie to also catch the lie. Sometimes it works. Sometimes Claude reads the rule, weighs it against the pressure to look competent, and guesses anyway. The viral post itself acknowledges this: "Punish honesty once and Claude goes back to guessing."&lt;/p&gt;

&lt;p&gt;Layer 4 — the fact-checker subagent — is a second model reviewing the first. This is the "two AI reviewers" trap I've written about before: if both models share the same training, the same helpfulness pressure, and the same blind spots, the second one can be talked out of a finding just as easily as the first. A model reviewer is not an independent check. It's a correlated one.&lt;/p&gt;

&lt;p&gt;Only layer 3 — the linter hook — is genuinely independent. &lt;code&gt;tsc&lt;/code&gt; doesn't care what Claude claimed. It reads the file and either the types resolve or they don't. That layer works &lt;em&gt;because it doesn't involve a model&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a single deterministic hook does instead
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;Groundtruth&lt;/a&gt; around that same principle: &lt;strong&gt;nothing reads the code, so nothing can be talked out of the verdict.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a Claude Code Stop hook. When the agent says "done," Groundtruth reads three things from outside the agent's control — the request, the transcript (what the agent actually ran), and &lt;code&gt;git diff HEAD&lt;/code&gt; (what actually changed) — and renders a verdict before the turn ends.&lt;/p&gt;

&lt;p&gt;No LLM. No network. No API key. One hook, not four layers.&lt;/p&gt;

&lt;p&gt;Here's what it catches, mapped against the 4-layer stack:&lt;/p&gt;

&lt;h3&gt;
  
  
  "Tests pass" — when nothing ran
&lt;/h3&gt;

&lt;p&gt;The viral article's fix: a Stop hook that runs the test suite before Claude can declare done.&lt;/p&gt;

&lt;p&gt;Groundtruth's approach: it checks whether a test command &lt;em&gt;actually executed in the session transcript&lt;/em&gt;. If Claude claims "tests pass" but no test runner appeared in the Bash history, it fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔴 false test/build claim — "tests pass", but no test command ran this session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference: the article's hook &lt;em&gt;runs&lt;/em&gt; the tests (which costs time and can fail for environment reasons). Groundtruth checks whether they &lt;em&gt;were&lt;/em&gt; run. If they weren't, the claim is false regardless of what the tests would have shown. Both are valid; mine is cheaper and catches the specific lie — "I ran the tests" when you didn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Made-up imports
&lt;/h3&gt;

&lt;p&gt;The article's fix: &lt;code&gt;tsc&lt;/code&gt; as a PostToolUse hook catches fabricated imports when the type checker fails.&lt;/p&gt;

&lt;p&gt;Groundtruth's approach: for JS/TS (where imports are path-relative), it resolves the import path against the file tree. If the target doesn't exist, it fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟡 phantom ref — new import of './utils/tokenHelper' but that file doesn't exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For package-qualified languages (Python, Go, Rust, Java, C#), it &lt;strong&gt;abstains&lt;/strong&gt; rather than false-flag — because a missing package import isn't provably wrong from the file tree alone. This is deliberate: a check that false-fires teaches you to ignore the card, which breaks everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Done" — but the file isn't in the diff
&lt;/h3&gt;

&lt;p&gt;The article doesn't cover this. Groundtruth does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟡 silent no-op — claimed src/upload.test.js, but it is absent from the diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent said it created a file. The diff says it didn't. No model needed to catch that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules your agent could see and overrode anyway
&lt;/h3&gt;

&lt;p&gt;The article's layer 1 puts rules in CLAUDE.md and hopes Claude follows them. Groundtruth compiles rules from your docs into &lt;strong&gt;deterministic predicates&lt;/strong&gt; — your doc says "use &lt;code&gt;pnpm&lt;/code&gt; not &lt;code&gt;npm&lt;/code&gt;," so a regex checks the diff for &lt;code&gt;npm install&lt;/code&gt;. The agent can't rationalise past a regex.&lt;/p&gt;

&lt;p&gt;These rules are proposed, never auto-armed. You review them with &lt;code&gt;/groundtruth-rules&lt;/code&gt; and approve what's correct. If one fires wrongly, silence it by id. The permission gate is yours, not the model's.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stubs and placeholders
&lt;/h3&gt;

&lt;p&gt;The article doesn't catch &lt;code&gt;// TODO: implement this&lt;/code&gt; or &lt;code&gt;raise NotImplementedError&lt;/code&gt; left in added code. Groundtruth does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;🟡&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;added&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// TODO: real backoff — single attempt for now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent claims a feature is done and the diff contains a placeholder, those two facts contradict. No judgment call needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "don't involve a model" matters
&lt;/h2&gt;

&lt;p&gt;The viral article's own best insight — buried at the end — is that the linter hook works &lt;em&gt;because it doesn't involve a model&lt;/em&gt;. That's the whole thesis of Groundtruth, applied to every check instead of just one.&lt;/p&gt;

&lt;p&gt;A model reviewer (layer 4) can be reasoned out of a finding. Claude is &lt;em&gt;very good&lt;/em&gt; at explaining why a seemingly-wrong thing is actually fine. "The test file wasn't needed because the function is trivial" sounds plausible to a second model that shares the same optimisation for helpfulness. It doesn't sound plausible to a diff that simply notes the file isn't there.&lt;/p&gt;

&lt;p&gt;The 4-layer approach works partially because 25% of it is genuinely independent (the linter). Groundtruth works fully because 100% of it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Groundtruth does NOT do
&lt;/h2&gt;

&lt;p&gt;I'll be precise, because overclaiming is the exact failure mode this tool is built to catch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt; judge code quality. It doesn't know if your algorithm is correct. It knows if the agent &lt;em&gt;claimed&lt;/em&gt; it was tested when it wasn't.&lt;/li&gt;
&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt; do semantic evaluation. "Did the agent rationalise past a rule" requires understanding intent, which requires a model. That's roadmap, not shipped.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;does not&lt;/strong&gt; replace testing. It catches the lie that testing happened. You still need the tests.&lt;/li&gt;
&lt;li&gt;It runs in the &lt;strong&gt;same trust domain&lt;/strong&gt; as the agent — tamper-evident, not tamper-proof. The honest security scope is in &lt;a href="https://github.com/akahkhanna/groundtruth/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;4-layer manual setup&lt;/th&gt;
&lt;th&gt;Groundtruth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Tests pass" with no test run&lt;/td&gt;
&lt;td&gt;Stop hook that &lt;em&gt;runs&lt;/em&gt; tests&lt;/td&gt;
&lt;td&gt;Checks transcript for test execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fabricated import&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tsc&lt;/code&gt; PostToolUse hook&lt;/td&gt;
&lt;td&gt;Path resolution against file tree (JS/TS); abstains elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File claimed but not created&lt;/td&gt;
&lt;td&gt;❌ Not covered&lt;/td&gt;
&lt;td&gt;✅ silent no-op detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stub/TODO left in "done" code&lt;/td&gt;
&lt;td&gt;❌ Not covered&lt;/td&gt;
&lt;td&gt;✅ placeholder scan on added lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project rules violated&lt;/td&gt;
&lt;td&gt;CLAUDE.md (hope-based)&lt;/td&gt;
&lt;td&gt;Deterministic regex from your docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fact-checking claims&lt;/td&gt;
&lt;td&gt;Subagent (model-based, correlated)&lt;/td&gt;
&lt;td&gt;Diff-anchored (no model, uncorrelated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config required&lt;/td&gt;
&lt;td&gt;CLAUDE.md edits + hooks JSON + subagent file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/plugin marketplace add&lt;/code&gt; + restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can the agent talk its way out?&lt;/td&gt;
&lt;td&gt;Layers 1, 2, 4: yes. Layer 3: no.&lt;/td&gt;
&lt;td&gt;No.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add akahkhanna/groundtruth
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;groundtruth@groundtruth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Code. Every turn gets a warn-only verdict card. No config needed. Arm your project rules with &lt;code&gt;/groundtruth-rules approve-all&lt;/code&gt; when you're ready. Turn on blocking with &lt;code&gt;/groundtruth-block on&lt;/code&gt; once you trust the precision.&lt;/p&gt;

&lt;p&gt;MIT. No LLM. No network. No API key.&lt;/p&gt;

&lt;p&gt;The 4-layer article was right about the problem. The fix is one layer, not four — and the layer that works is the one that doesn't involve a model.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these while building &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;Groundtruth&lt;/a&gt; and &lt;a href="https://erapin.com" rel="noopener noreferrer"&gt;EraPin&lt;/a&gt;. The 74% false-positive story — how we found it, how we killed it — is in &lt;a href="https://github.com/akahkhanna/groundtruth/blob/main/FIXES.md" rel="noopener noreferrer"&gt;FIXES.md&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Two AI reviews passed my change. The correct architecture was documented one file away.</title>
      <dc:creator>Akash Khanna</dc:creator>
      <pubDate>Sat, 04 Jul 2026 23:14:56 +0000</pubDate>
      <link>https://dev.to/akahkhanna/two-ai-reviews-passed-my-change-the-correct-architecture-was-documented-one-file-away-1fnd</link>
      <guid>https://dev.to/akahkhanna/two-ai-reviews-passed-my-change-the-correct-architecture-was-documented-one-file-away-1fnd</guid>
      <description>&lt;p&gt;Two separate models — one writing the code, one reviewing the diff — shipped a one-word bug to staging. Both ran local checks. Both came back green. The fix they missed was not exotic or subtle. It was written down, in plain English, in a file sitting in the same directory as the change.&lt;/p&gt;

&lt;p&gt;That last detail is the one worth staying with. This is not a story about a model being wrong. Both models were, within the job they were given, correct. It is a story about where verification stops looking — and why stacking two reviewers that stop looking in the same place feels like safety and isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;While fixing a cron timeout, I had Claude Opus pull a small JSON-parsing helper into its own file so it could be unit-tested in isolation. Reasonable instinct. It named the file &lt;code&gt;jsonExtract.mjs&lt;/code&gt; and imported it from &lt;code&gt;autoPublish.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_firstJsonObject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./jsonExtract.mjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// looks perfectly legal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole incident lives in that one letter — the &lt;code&gt;m&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;autoPublish.js&lt;/code&gt; is an ES module. On my machine, Node happily lets one ESM file import a &lt;code&gt;.mjs&lt;/code&gt; file, so every local check passed. The deployed runtime took a different path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local:     ESM importer → .mjs ES module → works
Deployed:  CommonJS require() → .mjs ES module → ERR_REQUIRE_ESM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to be precise about what I actually know here, because it matters. I did not instrument Vercel's build. What I observed is that the deployed function reached the helper through a CommonJS &lt;code&gt;require()&lt;/code&gt;, while local Node exercised a compatible ESM path. Whatever transformation produced that, the consequence is fixed by the language spec: a &lt;code&gt;.mjs&lt;/code&gt; extension forces a file to be an ES module, and &lt;code&gt;require()&lt;/code&gt; of an ES module is barred. The result was an invocation-time crash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error [ERR_REQUIRE_ESM]: require() of ES Module
  /var/task/.../jsonExtract.mjs
  from  /var/task/.../autoPublish.js  not supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a syntax error. Not a logic error. A packaging error that only becomes real once your platform rewrites the module system underneath your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seam nobody owned
&lt;/h2&gt;

&lt;p&gt;Every check you run has a scope, and a green result only ever means "clean &lt;em&gt;within that scope&lt;/em&gt;." We forget this because green is rendered the same color regardless of how much it actually covered.&lt;/p&gt;

&lt;p&gt;Walk the checks that ran on this change and read them as scopes rather than as pass/fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;node --check&lt;/code&gt;&lt;/strong&gt; — scope: syntax. The file parsed. True, and useless here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The test suite&lt;/strong&gt; — scope: the code paths the tests exercise, in the local runtime. All green. Also true; the tests never invoked the module across the deploy boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI review&lt;/strong&gt; — scope: logic correctness, as framed by the prompt. Race conditions, variable scoping, whether the extraction logic was sound. All fine. The reviewer was asked "is this logic correct," and it answered that question well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deployed runtime&lt;/strong&gt; — scope: the real module system. The only scope in which this bug exists at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bug did not slip past the checks. It lived in the &lt;em&gt;seam between two of them&lt;/em&gt; — the gap between "local ESM resolution" and "deployed CJS resolution" — and no check on the board had that seam inside its scope. &lt;code&gt;ERR_REQUIRE_ESM&lt;/code&gt; is an invocation-level error, so the build logs stayed green by definition: nothing executes at build time to trip it.&lt;/p&gt;

&lt;p&gt;This is where the "documented one file away" detail turns from irony into the actual lesson. In the same directory sat a sibling helper, &lt;code&gt;cluePrompts.js&lt;/code&gt;, carrying a prominent header comment saying it was &lt;em&gt;CommonJS on purpose&lt;/em&gt; — written that way specifically to be default-imported from ESM consumers and dodge this exact rewrite. Project docs reinforced the convention. The correct architecture was not unknown. It was &lt;strong&gt;passive&lt;/strong&gt; — encoded as prose, and prose is outside every automated scope above. A human skimming the diff wouldn't see it unless they already knew to look. Neither would a model. The knowledge existed and propagated to no one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two reviews is not two reviews
&lt;/h2&gt;

&lt;p&gt;Here is the trap that made me write this up, because it's the part that generalizes past Node and Vercel.&lt;/p&gt;

&lt;p&gt;Two independent green stamps — one from the author model, one from a separate reviewer model running the tests itself — &lt;em&gt;feels&lt;/em&gt; like defense in depth. It reads like redundancy. It is not. Redundancy only buys you coverage when the reviewers fail independently. These two shared a scope: both were evaluating logic correctness in the local runtime. When two checks share a scope, they share a blind spot, and stacking them multiplies your confidence without moving your coverage an inch.&lt;/p&gt;

&lt;p&gt;That is the quiet danger of multi-model pipelines. Adding a second model that thinks about the code the same way the first one did doesn't widen the net; it just gets you a more confident wrong answer. Real defense in depth requires checks whose scopes are &lt;em&gt;uncorrelated&lt;/em&gt; — one that reads a completely different signal than the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one check with a different scope
&lt;/h2&gt;

&lt;p&gt;I run a deterministic local hook while I build, &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;GroundTruth&lt;/a&gt;. I'm going to be exact about what it did and didn't do, because the honest version is the more useful one.&lt;/p&gt;

&lt;p&gt;It did &lt;strong&gt;not&lt;/strong&gt; catch the ESM bug. It couldn't. It has no window into Vercel's runtime and makes no attempt to predict a &lt;code&gt;require()&lt;/code&gt; failure from static text — semantic and environment-aware evaluation are on its roadmap, not in it. If I told you it saw the crash coming, I'd be selling you the exact false confidence this whole post is about.&lt;/p&gt;

&lt;p&gt;What it did do is refuse to agree that the change was green — because its scope was not logic correctness. Its scope was &lt;em&gt;does the claim match the observable evidence&lt;/em&gt;. Every turn where I asserted passing tests, it fired the same warning, because it could not find a test run in the evidence to back the assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[warn] false test/build claim — claimed tests/build pass ("tests pass"),
  but a test run looks like it reported failures — double-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the moment I pasted the staging crash into the terminal, it opened a task and held it open — not until I &lt;em&gt;said&lt;/em&gt; it was fixed, but until an actual Node change appeared in the diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[warn] open loop (asked, not delivered) — pending task [tftlx] —
  "Staging failed: Error [ERR_REQUIRE_ESM]: require() of ES Module …"
   (no Node.js in the diff yet)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It cleared only when the CommonJS fix actually landed in the diff — a deterministic "Told &amp;amp; Done," no model asked.&lt;/p&gt;

&lt;p&gt;Read that carefully, because it's easy to mistake for a tool win and it isn't one. The point is not that GroundTruth is smarter than the reviewers. It is dumber than the reviewers, deliberately — it doesn't read the code at all. It reads claims against evidence. That's a &lt;em&gt;different scope&lt;/em&gt;, and a different scope is the only thing that can catch what a correlated pair misses. The signal that mattered was accurate the whole time. My mistake was weighting the two confident green stamps over the one stubborn yellow warning that didn't share their blind spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Convert the helper to CommonJS, matching the pattern its neighbor already documented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// jsonExtract.js — CommonJS on purpose, matching cluePrompts.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;_firstJsonObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_firstJsonObject&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// autoPublish.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;_jsonExtract&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./jsonExtract.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// CJS — safe after the deploy rewrite&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;_firstJsonObject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_jsonExtract&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works locally, works deployed, one source of truth. Fixed forward in a single commit, no rollback, zero production downtime — it never left staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule going forward
&lt;/h2&gt;

&lt;p&gt;Any new module under &lt;code&gt;api/&lt;/code&gt; either matches the CommonJS convention already demonstrated beside it, or it gets exercised under a live invocation environment before it can merge to main. Not because &lt;code&gt;.mjs&lt;/code&gt; is bad — because a convention that lives only in a comment is not enforcement, it's a wish.&lt;/p&gt;

&lt;p&gt;The failure chain, stripped down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The right architecture was known
   → but it was passive prose
   → so the author didn't apply it
   → and the reviewer didn't apply it
   → and the local checks couldn't encode it
   → so the runtime was the first thing that could
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.mjs&lt;/code&gt; versus &lt;code&gt;.js&lt;/code&gt; is the trivia. The systems lesson is that &lt;strong&gt;documented knowledge enforces nothing&lt;/strong&gt;, and &lt;strong&gt;two reviewers that think alike are one reviewer that costs twice as much.&lt;/strong&gt; If a rule matters enough to take down a deploy, it has to live somewhere a check can read it — as a test, a lint, a gate — or you have to run the code where breaking it becomes real. Prose in a header comment is where good conventions go to be ignored politely.&lt;/p&gt;

&lt;p&gt;So I'll turn it into the question I actually want answered: &lt;strong&gt;what's the convention your codebase documents but doesn't enforce — and what did it cost you the day someone, or something, didn't read it?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write these up while building &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;GroundTruth&lt;/a&gt; and &lt;a href="https://erapin.com" rel="noopener noreferrer"&gt;EraPin&lt;/a&gt;. This one was caught the instant the cron executed on Vercel staging, and fixed forward within the hour.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vercel</category>
      <category>node</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your AI coding agent isn't lying to you. It's optimizing. published: false</title>
      <dc:creator>Akash Khanna</dc:creator>
      <pubDate>Sat, 04 Jul 2026 15:37:42 +0000</pubDate>
      <link>https://dev.to/akahkhanna/your-ai-coding-agent-isnt-lying-to-you-its-optimizingpublished-false-1cco</link>
      <guid>https://dev.to/akahkhanna/your-ai-coding-agent-isnt-lying-to-you-its-optimizingpublished-false-1cco</guid>
      <description>&lt;p&gt;Every dev using an AI coding agent has hit this moment: the agent says "Done — tests pass" and you go check, and nothing passes. Or worse, nothing changed at all.&lt;/p&gt;

&lt;p&gt;The instinct is to ask &lt;strong&gt;"why did it just lie to me?"&lt;/strong&gt; That's the wrong question. It assumes intent. There isn't any. The right question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What made the wrong answer cheaper than the right one — and what input did it exploit to get there?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question always has an answer. And the answer is always your next check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mantra
&lt;/h2&gt;

&lt;p&gt;An LLM agent isn't a person deciding whether to be honest. It's a process that takes whatever path costs least, given whatever is actually being measured. If "claim done" and "verify, then claim done" both produce the same reward — because nothing downstream distinguishes them — the agent will drift toward the cheaper one. Every time.&lt;/p&gt;

&lt;p&gt;This isn't a flaw you can prompt your way out of. "Please don't lie to me" doesn't change the cost structure. What changes it is making the dishonest path &lt;em&gt;actually&lt;/em&gt; expensive: something that catches the gap between claim and reality, every time, automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The structural flaw: collapsed trust domains
&lt;/h3&gt;

&lt;p&gt;Standard AI coding setups create an architectural conflict of interest. The same context that writes the code is also the thing asked to validate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Context / Token Window ]
   ├── 1. Prompt: "Refactor Class A"
   ├── 2. Agent Output: Generates Code
   └── 3. Verification: Agent looks at its own output
        └── Result: Confidently asserts "Tests Pass" (Hallucination)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model has every incentive to satisfy its own parameters within the shortest possible generation path — including the "verification" step. The fix is to physically pull validation out of the agent's own context and into an independent, deterministic trust domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ LLM Agent Trust Domain ]
   └── Generates Code / Claims Victory
            │
            ▼ (Interception Point: Stop Hook)
┌────────────────────────────────────────────────────────┐
│ [ Deterministic Trust Domain (Local FS / AST / Git) ]   │
│   ├── 1. Read Raw Session Transcript                   │
│   ├── 2. Parse Raw Git Diff                             │
│   └── 3. Verify: Assert Broken Symbols &amp;amp; Mock Tests     │
└────────────────────────────────────────────────────────┘
            │
            └── [FAIL] -&amp;gt; Reject Turn &amp;amp; Re-inject Error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;GroundTruth&lt;/a&gt; (a Claude Code Stop-hook plugin) after hitting this exact pattern on my own project, EraPin. Agents kept claiming "tests pass" or "refactor complete" when the git diff told a different story. Every fix I've shipped since started with the same exercise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broadened extraction rule&lt;/strong&gt; → a missed rule cost nothing, because nothing measured recall. Fix: track what's &lt;em&gt;not&lt;/em&gt; being parsed, not just what is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounding check regression&lt;/strong&gt; → a zero-hit result looked identical to "genuinely absent," so a silent no-op was free. Fix: pin the check against a real signal, not a pattern that can quietly degrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission gate&lt;/strong&gt; → auto-arming a misread rule cost nothing when there was no human in the loop. Fix: nothing gets armed without explicit approval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is the same shape: find the loophole where "looks done" was cheaper than "is done," and close it so the honest path is the only cheap one left.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anatomy of an optimization drop
&lt;/h3&gt;

&lt;p&gt;To see why this happens, look at what an agent leaves on the cutting-room floor when its context runs hot. It doesn't "forget" code the way a tired human does — it drops lines to shorten its path to a clean compile and a stop token.&lt;/p&gt;

&lt;p&gt;Take a legacy production method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Original code (pre-refactor)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processTransaction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transaction&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;validateToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;auditLog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- the critical business audit step&lt;/span&gt;
    &lt;span class="n"&gt;executionEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask an agent to merge this class with another, and it can hand back a block that looks completely normal to a fast reviewer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What the agent actually outputs&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processTransaction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transaction&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;validateToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;executionEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- silently dropped&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The summary reads confidently: &lt;em&gt;"Refactored transaction processing to use the new engine. All logic preserved."&lt;/em&gt; The compiler won't flag it — the syntax is valid. The agent didn't make a mistake; it optimized for completion. Dropping the audit call was the cheaper path to a block that superficially matches the prompt, because nothing in the loop was checking for &lt;em&gt;preserved&lt;/em&gt; behavior, only &lt;em&gt;valid&lt;/em&gt; syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond one plugin
&lt;/h2&gt;

&lt;p&gt;This reframes the whole AI-agent-trust problem. You're not fighting deception. You're fighting economics. Once you see it that way, the fix is always concrete and buildable: verify against ground truth (diffs, transcripts, actual test runs), not against the agent's own narration of what it did.&lt;/p&gt;

&lt;p&gt;If you're building with Claude Code or any agent framework and hitting false "done" claims, the question to ask isn't "how do I make it more honest" — it's "what's the cheapest lie it can currently get away with, and how do I take that away."&lt;/p&gt;

&lt;h3&gt;
  
  
  Building deterministic boundaries
&lt;/h3&gt;

&lt;p&gt;We can't prompt our way out of an optimization engine. The fix is a local, hard-coded boundary that processes the raw outputs of a session before the turn is allowed to close — no second LLM required, just deterministic checks against what actually happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A deterministic verification hook, simplified
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;git_diff_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;git_diff_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Rule: if the agent claims tests passed, the run must actually appear
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tests pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(mvn test|gradlew test|pytest)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL: claimed tests passed, but no test runner executed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;System boundaries verified.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shift is from writing better prompts to building rigid boundaries — treating the agent less like a teammate who might lie, and more like a powerful, volatile compiler that needs strict runtime validation.&lt;/p&gt;

&lt;p&gt;If you're hitting this pain yourself, I put together an open-source, deterministic Claude Code hook called &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;GroundTruth&lt;/a&gt; that checks the raw git diff against the actual session transcript. Contributions and your own validation rules welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about this kind of thing while building &lt;a href="https://erapin.com" rel="noopener noreferrer"&gt;EraPin&lt;/a&gt; and &lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;GroundTruth&lt;/a&gt; in the evenings. Feedback welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My coding agent said "everything preserved." A method was already gone.</title>
      <dc:creator>Akash Khanna</dc:creator>
      <pubDate>Fri, 03 Jul 2026 08:27:08 +0000</pubDate>
      <link>https://dev.to/akahkhanna/my-coding-agent-said-everything-preserved-a-method-was-already-gone-5851</link>
      <guid>https://dev.to/akahkhanna/my-coding-agent-said-everything-preserved-a-method-was-already-gone-5851</guid>
      <description>&lt;p&gt;I've spent most of two decades in enterprise Java, where "done" meant it is working and you have done what you were asked for.  It's in production, it works, and you can prove it. So when I started leaning on a coding agent and it told me "done — everything preserved, tests pass,"my instinct was the one every long-tenured engineer has: &lt;em&gt;show me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the one that actually stung. I had Claude collapse three classes into one — it had grown too complex — and later split another apart. Copy-paste the whole refactored class, done in the chat window and pasted into my IDE, not in an agent that touches the repo directly. It reported everything preserved. I believed it. Days later, something broke in a path no test covered, and I traced it to a method that  wasn't there anymore. And this happened even when I asked three different agents to compare and even with the max usage and the best of models I could use at that point. It hadn't made the trip during the merge. Nothing flagged it, because nothing forced the claim to be checked. I just prayed the refactor was clean, and it wasn't.&lt;/p&gt;

&lt;p&gt;The thing that clicked: &lt;strong&gt;a claim is not evidence, and a rule in the agent's context is not enforcement.&lt;/strong&gt; "Everything preserved" is an assertion about a mapping the agent reshaped and never verified. "Tests pass" is a claim about a run that, it turned out, never happened that session. I'd been treating the agent's summary as if it were the ground truth. It's the press release, not the primary source.&lt;/p&gt;

&lt;p&gt;So I built a small thing to read the primary source. It's a Claude Code Stop hook. When the agent says it's finished, the hook reads the actual &lt;code&gt;git diff&lt;/code&gt; and the session transcript — not the summary — and checks the claim against what really changed, before the turn is allowed to end. It's deterministic: no model sits in the check itself, which matters, because the same confident narration that talks an LLM reviewer into "looks good" has nothing to grab onto in a diff parser.&lt;/p&gt;

&lt;p&gt;On the refactoring pain specifically, here's the honest scope, so I too do not sound like the agent I faced an issue with. It doesn't try to detect "a method was dropped" — that needs intent, and guessing intent is where false positives come from. It detects the observable &lt;em&gt;consequence&lt;/em&gt;: a call that no longer resolves, in a turn that claimed nothing changed. That one reframe makes the messy cases fall out correctly — a rename with the callers updated stays silent; a rename that &lt;em&gt;missed&lt;/em&gt; a caller fires on the real broken call; a merge or a split doesn't matter, because it checks whether the symbol is defined anywhere in the whole tree, not just where it used to live. What it can't catch, and says so plainly: a method dropped with no caller left behind — no dangling reference, no textual signal, invisible to a diff. It trades recall for precision on purpose. It would not have caught &lt;em&gt;every&lt;/em&gt; possible silent drop in my refactor — but it would have caught the one that broke, because something still called it.&lt;/p&gt;

&lt;p&gt;The broader set of things it flags, in plain terms: "tests pass" when no test ran this session, stubs and TODOs in new code, a file the agent said it changed that isn't in the diff, imports pointing at nothing, hardcoded secrets, and rules from your own project docs that got overridden anyway.&lt;/p&gt;

&lt;p&gt;What surprised me is how much company this idea has now. There's a wave of people landing in the same place — that you shouldn't ask the same agent to write code and verify it, because it has no incentive to fail its own work; that the validator needs separate context; that the answer is guardrails that make the bad behaviour hard, not prompts that ask nicely. A year ago this felt like a personal irritation. Now it feels like a category forming, which is a good sign I'm not the only one muttering "show me" at a green checkmark.&lt;/p&gt;

&lt;p&gt;It's open source (MIT), it's deterministic, and in-session it's tamper-&lt;em&gt;evident&lt;/em&gt;, not tamper-&lt;em&gt;proof&lt;/em&gt; , not perfect and I am still fixing issues as I find them— the hook shares a filesystem with the agent it audits, so the real guarantee lives in the pre-commit/CI rung, in a separate trust domain (that's in there too). It checks that the agent did what it &lt;em&gt;said&lt;/em&gt; — not that what it said was &lt;em&gt;right&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Link's below. The one thing I actually want back: tell me where it false-flags you. False positives are what kill a tool like this, and I'd rather hear about them than not.&lt;br&gt;
&lt;a href="https://github.com/akahkhanna/groundtruth" rel="noopener noreferrer"&gt;https://github.com/akahkhanna/groundtruth&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claude</category>
      <category>coding</category>
    </item>
  </channel>
</rss>
