I build with AI assistants every day. Sixteen applications' worth of architecture has come out of directing them. I am not writing this to argue against the practice, I would not get a fraction of this work done without it.

I am writing it because of a specific failure mode that nobody warns you about properly. It is not that the assistant writes bad code. Modern assistants write good code. It is that the assistant's report on its own work is generated by the same process that produced the work, with the same confidence, and no independent access to the truth. A model that wrote a bug will describe the system as though the bug isn't there, because from inside the generation there is nothing marking it as wrong.

What I was told

After a persistence rewrite, my agent reported:

SSR verified: <h1> present, 17,070 chars of body text, 17/17 checks pass. Persistence read/migration/corrupt-recovery all tested working.

Everything in that sentence was true. The page was also broken.

What was actually true

Open the page in a browser:

TypeError: Cannot read properties of undefined (reading 'length')
  at ChatModal (ChatModal.tsx:378:25)

Line 378 is {messages.length ...}. messages was undefined, not empty. The chat modal was throwing an unhandled runtime error on every render, and had been throwing it while the suite reported all green.

Why the suite couldn't see it

This is the part worth internalising. The verification script did exactly what it was written to do:

curl -s "$URL" | grep -qi "<h1"

It fetched server-rendered HTML and asserted on its contents. A crash that occurs during client-side hydration happens after that HTML is delivered, in a browser the script never opens. The suite was structurally incapable of observing the failure it was being trusted to cover.

So a crawler got a perfect page. A human got a broken one. And the dashboard said 17/17.

That sentence is the whole article.

The root cause, which is the more interesting part

The bug came from code I had been given by a different AI assistant, which recommended replacing an abandoned persistence library with custom middleware. The middleware persisted a narrow allowlist of fields, deliberately, to prevent a separate serialization bug, and fed them back through Redux's preloadedState.

The defect: configureStore's preloadedState replaces a slice's state. It does not merge with the reducer's initialState.

So persisting { chat: { isOpen: false } } gave the chat slice a state of exactly { isOpen: false }. No messages. Every field the allowlist didn't name became undefined on reload.

One root cause with two symptoms: the crash here, and a null promo banner I had spent a separate debugging session on. The banner slice rehydrated without its queue, so the selector returned null, so the component rendered nothing, which is precisely what its debug probes had been reporting all along.

Note what happened across the two systems. One AI wrote a subtly wrong hydration path. A second AI installed it and certified it green. Neither was careless. The failure was that both were reasoning about the system rather than observing it.

The fix

Merge persisted fields into the reducers' initial state instead of substituting for them. Run the root reducer with undefined to obtain the canonical initial state, then overlay only the persisted keys. Plus a dev-only assertion comparing hydrated keys against initial keys, so this class of bug fails loudly at boot rather than at the first component that reads a missing field.

The lesson

Design the check so it can observe the failure mode you actually care about. If the risk is a client-side crash, the check has to load a browser and fail on console errors. HTML assertions cannot do it, and a suite that passes while the product is broken is worse than no suite, because it converts uncertainty into false confidence.


The rules I work by now

Every one of these came from an incident, not from a blog post.

  1. **Ask for evidence shape, not status. **

  2. **Check the premise when the fifth hypothesis fails. **A wrong answer gets corrected. A wrong premise generates unlimited plausible next steps, each feeling like progress. If five things have been ruled out, the sixth is not the problem.

  3. **Verify with an instrument the model doesn't control. **A screenshot. Devtools. A real browser. The agent's sincere belief that its probes weren't firing was contradicted by four lines on my own screen.

  4. **Design the check to observe the failure mode you care about. **

  5. **Read your own diff first. **Then test. Then theorize about someone else's code. Three of my four incidents would have been caught by that single step.

  6. **Treat numeric coincidence as a lead, never a conclusion. **768 is close to 766. That is a reason to test a hypothesis, not a reason to believe it. Pattern-matching on similar numbers is the model's native mode and the thing you have to supply judgment against.

  7. **Revert the residue of wrong theories. **Code added while chasing a hypothesis that turned out to be false should come out when the hypothesis does. It doesn't announce itself later as speculative.

Why I publish this rather than hide it

Every engineer using AI assistants in 2026 is going to hit these. Most will hit them quietly and conclude either that AI is useless or that it is magic. Neither is true, and both are expensive.

The useful position is the third one: these tools produce excellent work and unreliable self-reports, and the discipline that separates the two is learnable, specific, and mostly consists of looking at the actual system with your own eyes.

I would rather show you the four times I caught it than tell you I'm careful.