The trouble is not that the model produces bad code. Most of the time it produces reasonable code. The trouble is that the failures are quiet, confident, and shaped exactly like success. Below are the ones we hit most often, and what we actually do about each.
It refactors things you did not ask it to touch
We ask for a fix to one function. We get back a rewritten module, with variables renamed for taste and helpers reorganized for a symmetry that was not requested. The new code may even be better. That is not the point. The diff is now too large to review honestly, and an unreviewable diff is the real hazard, because an unreviewable diff gets approved. We push back by scoping the request to a single function and rejecting the rest, however tidy it looks.
It invents architecture and does not tell you
Given room, the model will make a structural decision: a queue here, a cache there, a new boundary between two things that used to be one. It does not surface this as a decision. And when we question it, it defends the choice consistently, because consistency is what it optimizes for. We have learned to read that steadiness as a warning rather than a reassurance. Confidence is not evidence. A model that never wavers is not more correct, only more fluent.
It moves the layout four pixels
This one is our least favorite. The model fixes a genuine bug and, on the way through, nudges a element four pixels. Nothing throws. No test covers it, because tests rarely assert on pixels. You find out from a user, or from a screenshot comparison you had the discipline to set up in advance. We treat visual regressions as inevitable and catch them mechanically rather than hoping to notice them by eye.
It quietly drops the fifth instruction
Hand it five instructions and it will do four of them beautifully, then skip the fifth without comment. There is no error and no apology. This is the silent-omission problem that shows up in any long prompt, and it is one reason we think about prompts as infrastructure that deserves the same review a deployment script gets. We check every instruction back against the output by hand.
It abstracts when a function would do
Ask for a small thing and you may get a factory, a config layer, and a strategy pattern, because those patterns are heavily represented in what the model learned. The abstraction is not wrong so much as premature. We push it back toward the plainest thing that works and add structure later, when a second caller actually exists.
It is confident and wrong about the recent
The model is most fluent about what was widely written about, and least reliable about the API that changed last quarter. Old patterns arrive polished and current ones arrive slightly off, stated with the same composure. For anything that shipped recently we read the source, not the summary.
What actually mitigates this
Almost none of the fix is better prompting. It is process. We keep diffs small, so review stays honest. We give narrow instructions, one change at a time. We write tests that assert on behavior rather than shape. We use prerender and screenshot diffing to catch layout drift no assertion would. And a human reads every line and is willing to say no. Several patterns the model skips are ones we now check by rote, which is part of why we keep a production checklist of the things AI skips. The same habits run through every project on our case studies.
The teams that get hurt are not the ones who use AI. They are the ones who treat plausible output as reviewed output.
None of this is an argument against using AI. We use it every day and it makes us faster. It is an argument against trusting it. The output is a draft written by something confident, and a draft is exactly as good as the person willing to read it closely and reject the parts that do not hold.