Red is Better than Green
Bad code makes me sad.
And it's not the kind that doesn't run - that's part of the process - but the kind that makes you wince when you see it; the kind that makes you sigh and say "oh no, not again".
If you squint and look at a single file or diff, you might not see it.
It passes checks. It works. It might actually look pretty good.
But when you zoom out and look at how the code interacts with the rest of the codebase - that's when you start feeling it.
I've been noticing the same things in Claude's code for months. Like try-catches that swallow errors - the stack trace gone - reduced to atoms. Functions rewritten from scratch, again and again, instead of importing the one that already exists three files over. Types that are just any wearing a trench coat. Fallback values that silently mask bugs instead of letting them surface.
None of it is ever broken. It passes lint checks. It passes tests. It runs. It's just code I'd never want to maintain, and it's the same kind of bad - every single time. I've been working as a human linter for every diff, correcting for patterns I couldn't quite explain.
It's been draining me. So I went looking for the why.
Learn to Yearn
It starts with pretraining. The model ingests trillions of tokens of text and code, learning statistical patterns and associations. It learns certain patterns of code that are more common than others - and it learns to reproduce those patterns.
Then comes posttraining - reinforcement learning that fine-tunes the model to maximize a reward signal. The methods are ever-changing, but the core dynamic is always the same: the model optimizes for a score, and scores can be gamed. This is called reward hacking, and for code generation it can be condensed into this: runs and doesn't throw = good, blows up = bad, fewer tokens = fast = good.
That's where the pattern clicked for me. Claude optimizes for code that looks safe and tasks that finish fast. It doesn't care if the code is well-structured, because nothing in the reward signal measures that. Why not reward maintainability? Because maintainability is a property that only reveals itself over time, in a constantly evolving system. You'd need the model to live with its own code for weeks, modify it, extend it, and feel the pain of its own decisions. Evals are not like that. So the reward function measures what it can: a proxy metric.
The model learned that wrapping everything in a try-catch is cheaper than understanding whether an exception can actually occur. It's working exactly as designed. And it is exhausting.
Red > Green
Claude will finish the task. And it will finish by getting Green. It will produce something - and fast. Every signal in its training points it there - task complete, no errors, move on. Whether it produces something correct and good - that's where we need checks.
And you already have them - Linting and Tests.
Claude can run any of these, get a red signal, and fix its own mess before you ever see it. The fast ones matter most (same logic as the testing pyramid, which gets followed about as often as the food pyramid): if the linter catches a swallowed error in seconds, that's a problem Claude deals with instead of a problem I find in code review at 11pm.
The more I delegate to agents, the more assumptions and decisions they make. I've been burning my own brain catching them. Time for the machines to do that.
Claude yearns for the Green. If we want code that doesn't suck, we have to give it Red.
Unguard
"Just add more linters and tests, dummy!" is too vague a suggestion. I keep seeing the same categories of slop, so it makes sense to target those first.
The checks we actually need are closer to application logic than a general-purpose linter can and should handle. For example, whether a try-catch is appropriate depends on the logic of the code - a basic linter can't and shouldn't know that, but a check that runs after the code is generated can analyze it with richer context.
I couldn't find one that matched what I actually needed, so I started building one.1 It's called unguard - a static analyzer for TypeScript that flags defensive coding patterns where the types already prove correctness. Just run npx unguard. I run it as a second linter step - lint, then unguard, then type checks - as part of a "verify your work" instruction in claude.md. Claude hits it before I ever see the diff.
If Claude duplicates a type because it was too lazy to figure it out - we catch that. If Claude writes overdefensive code - we force it to prove that it needs to be so defensive. Unguard won't care if the code runs, but Claude will definitely care whether the code should have been written that way.
The rules map directly to the slop I keep seeing, and I think they go deeper than just fixing gripes.
Just think about it. If we have proper rules against these patterns, the functions that Claude writes become more dependable, the types it creates become more accurate, and the code it generates becomes more maintainable. By forcing Claude to confront the discomfort of Red, we force Claude to write better code. This better code then reinforces the rules, because Claude will match the patterns it sees in the codebase. It's a positive feedback loop.
Some powerful examples:
no-empty-catchand its cousinno-catch-returnhelp stabilize the expected error handling patterns in the codebase, making debugging and maintenance more predictable.no-any-castforces the model to create accurate types instead of lazily slapping any on things. Huge impact on code quality - and it gets interesting when you see what happens next.duplicate-function-declarationcatches when the model reimplements a function that already exists, which is a common pattern when it doesn't want to take the time to find and understand the existing function. This encourages code reuse and consistency across the codebase, so DRY principles are actually followed.optional-arg-always-usedflags when the model creates an optional argument that is actually always used, which is a sign of bad separation of concerns and can lead to spaghettification.2
And many more.
These compound:
Take no-any-cast. Claude's first instinct is to fix it the easiest way - hardcoding types at function boundaries. But that trips no-inline-type-in-params and duplicate-type-declaration. So now it has to actually think about the type structure. One rule forced three corrections, and the codebase got real type safety out of it.
Every pattern enforced gives more in-context support for better code. Rules enforce better code, which opens opportunities for more powerful rules, which enforce better code - a virtuous cycle.
I want feedback
Use the tool. npx unguard. Tell me what patterns you see that I haven't encoded yet. If you have a pattern of slop that you keep seeing in AI-generated code, open an issue and we'll discuss.
Footnotes
-
Many people are thinking about this too! react-doctor does something similar and very interesting for React. plankton released with a similar goal of catching "AI code smells" for Python. ↩
-
Whether of the code or your brain is left as an exercise for the reader. ↩