You ask for a fix to CSV handling and get a base importer class, a handler registry and scaffolding for formats the project doesn’t have. The original task is solved, but the result has become harder to read and maintain. Good code principles help you check what an AI agent has produced: what the task really needs, and what only makes the project more complicated.
It pays to give Claude Code, Codex and other agents limits on complexity. Before a review, ask yourself three main questions: is the change needed for the current task, has the expected behaviour been preserved, and how has the result been checked? The full checklist is at the end of the article.
The ten principles below help answer these questions. Start with YAGNI and KISS, and apply DRY together with AHA: fighting repetition shouldn’t turn into mass-producing universal constructs. Each point has a “Check for the agent” line: a question worth asking of the diff.
Good code principles: YAGNI, KISS, DRY and AHA
1. YAGNI: don’t build for imagined needs
You Aren’t Gonna Need It: don’t add a capability until it’s required. If the task is to export a report to CSV, Excel support, swappable storage providers and a plugin system aren’t needed yet.
At the same time, YAGNI is no excuse for giving up on reliability. Input validation and a test for the bug you’re fixing belong to the current task, not to fantasies about the future.
Check for the agent: which requirement of the current task justifies this new class, parameter or file? If the answer is “it’ll come in handy later”, that’s a reason to put the change off.
2. KISS: choose the simplest solution that’s enough
Keep It Simple, Stupid, or in its gentler version Keep It Short and Simple: simplicity matters more than architectural showmanship. For a local data transformation, a plain function can be clearer than a chain of factories and strategies.
A simple solution isn’t necessarily the shortest. An explicit check with a clear error message is better than a clever one-liner. The criterion is how much you need to know to change the behaviour safely.
Check for the agent: can the task be solved with fewer new entities: classes, parameters, layers? If so, what justifies the more complex route?
3. DRY: don’t duplicate a single business rule
Don’t Repeat Yourself is above all about a single source of knowledge, not a ban on similar-looking lines. If the rule for calculating a fee is copied into several places, it’s easy to fix it in some of them and miss others. Such a rule is moved to one place (a function, a constant or a setting) and referred to from there.
But two similar loops don’t yet prove they need a shared function. First find out whether they express the same rule and must change together.
Check for the agent: if this rule changes tomorrow, in how many places will the code have to be edited? The right answer is one.
4. AHA: wait for a clear abstraction
Avoid Hasty Abstractions: steer clear of premature generalisation. If a shared handler needs the flags is_preview, skip_validation and special_case, it may have merged different tasks.
Leaving two small functions for a while is sometimes wiser than tying them together with the wrong model. Sandi Metz puts it briefly: duplication is cheaper than the wrong abstraction. Remove the repetition once it’s clear what exactly is shared and where the differences lie.
Check for the agent: how many flags and conditional branches have appeared in the shared function? If every new task adds another, it’s better to break the abstraction back up into separate functions.
SRP and module boundaries: how not to overcomplicate the architecture
5. SRP: one main reason to change
Single Responsibility Principle: a module should have one main reason to change. Robert Martin clarifies that the reason for change is people: the requirements for a module should come from one group, for example from accounting or from the mailing team. So changing the report format shouldn’t force you to edit the email delivery logic.
This isn’t a requirement to split every action into its own class. A small script can stay a single file with clear functions. A boundary belongs where it helps you change behaviour independently, not wherever you can think up another name.
Check for the agent: who will the next changes to this module come from? If from two different groups, it’s worth splitting. If from one, no new class is needed.
6. High Cohesion, Low Coupling: keep related things together, make dependencies explicit
High cohesion within a module means its parts solve a closely related task. Low coupling between modules means that changing one doesn’t require knowing the internals of the others. Martin Fowler gives a simple test: if changing one module forces you to change another, there’s coupling between them.
For example, it’s useful to keep the rules for calculating an order’s cost together. The notification module only needs to receive the final amount: it has no need to read the internal calculation tables. Everything a module needs from its neighbours it receives explicitly, through parameters and calls, not through global variables or someone else’s internal data.
Check for the agent: the rule cuts both ways. Is the agent dumping everything into a shared utils, or scattering neighbouring steps of a single operation across many files for no reason?
Fail Fast and POLA: explicit errors and predictable behaviour
7. Fail Fast: catch the problem at its source
Fail Fast: report an error immediately, at the point where it occurred. Jim Shore is upfront about it in his 2004 article: this won’t reduce the number of bugs, but it will make them much easier to find.
If an operation needs a configuration parameter, check it before starting work. If the input file is corrupted, say so, rather than returning an empty result that looks like successful processing.
Code that catches any exception and silently carries on is especially suspect. The error will surface later, when the data is already corrupted, and the cause will be hard to find. Agents have their own version of this trap: when a test fails, an agent may wrap the code in try/except or substitute a default value such as config.get("api_key", ""), just to make the test pass. The test is green, and the error has simply moved further down the line.
Fail Fast doesn’t mean “bring down the whole service on any error”. The error needs to be detected early and handled at a suitable boundary: reject the request, isolate the failed job or end the operation with a clear status.
Check for the agent: are there new exception handlers and default values in the diff? For each one it should be clear which error it handles and why staying silent here is safe.
8. POLA: no surprises for the calling code
Principle of Least Astonishment. A function’s name and contract should make it clear what to expect from it.
get_user() shouldn’t quietly create a user, and a preview mode shouldn’t send real emails. If a side effect is necessary, it has to be made explicit in the interface and documentation: a function that may create a user is more honestly called get_or_create_user().
Check for the agent: has an existing contract (name, parameters, return value, side effects) changed for the convenience of the local implementation? If the task didn’t call for it, the change is unnecessary.
Optimisation and tidying: where to stop
9. Premature Optimization: don’t optimise without measurements
Donald Knuth wrote the line about premature optimisation being the root of all evil in 1974, and it carries an important caveat: it’s about small improvements that aren’t needed in about 97% of cases. Caching, parallel execution and complex data structures add not only speed but new risks of bugs.
First establish that a performance requirement isn’t being met, then find the bottleneck by measuring. After optimising, measure again and check that the behaviour is preserved.
This isn’t a call to pick a knowingly inefficient algorithm. Knuth himself warns that in the remaining critical 3% the chance to speed up the code mustn’t be passed over. The point is not to pay in complexity for a speed-up whose necessity hasn’t yet been established.
Check for the agent: which measurement showed that this is the bottleneck? Agents are keen to add caching and parallelism “for speed”. Without before-and-after figures, such a change is better removed.
10. Boy Scout Rule: improve a little at a time, don’t take over the whole project
The Boy Scout Rule was popularised by Robert Martin: leave the code a little cleaner than you found it. Clarify a variable name, remove a branch that truly can’t be reached, fix a comment next to the logic you’re changing.
But this rule has a limit. A bug-fix task doesn’t give the agent permission to mass-rename modules or rewrite a neighbouring subsystem. A big clean-up is better proposed separately, so that it can be reviewed and, if necessary, rolled back independently.
Check for the agent: do the small improvements touch only the places the agent was changing for the task anyway? Everything else goes in a separate proposal, not in the same diff.
When DRY conflicts with KISS and AHA
Imagine two functions: one prepares a preliminary order calculation, the other the final one. At first they look alike. The agent merges them for the sake of DRY, then adds flags for the differences: the preliminary calculation has no payment, the final one requires checks, and then a third scenario appears with its own rounding.
On paper there’s less repetition. In practice there are more permitted combinations of parameters, and the developer now has to remember how they relate.
The criterion for choosing is a shared rule, not outward similarity. If both operations must calculate tax the same way, that calculation is worth extracting. If the operations themselves change for different reasons, the functions that drive them can stay separate. That way DRY protects knowledge from duplication, while AHA and KISS protect the code from the wrong merge.
A similar trap is demanding all of SOLID from a small Python script. SOLID is a set of five design principles, and SRP is the S in it. SRP can help separate reading a file from the calculation, but on its own it doesn’t call for a dependency container and a hierarchy of interfaces. The scale of the solution should match the scale of the task.
Our practical priority: first the required behaviour and reliability, then clarity, and only then generalisations and optimisations that can by then be justified.
A prompt for Claude Code and Codex: how to limit unnecessary code
Below is a starter template. We haven’t measured how it changes agents’ behaviour, so adapt it to your project. You can put it in AGENTS.md, CLAUDE.md or the text of a specific task:
Follow the YAGNI, KISS, DRY, AHA, SRP and Fail Fast principles.
First study the existing code and reuse suitable
solutions. Don't add functionality, abstractions or dependencies
unless the current task needs them.
Don't duplicate business logic, but don't complicate code for the sake of DRY.
Don't hide errors, and don't optimise without a measured need.
Preserve existing contracts and predictable behaviour.
Don't change code outside the task. Small improvements are allowed only
in places you're changing anyway; propose large refactoring
separately. When principles conflict, choose the simplest
solution that doesn't compromise reliability.
After making changes, run the relevant tests and checks.
State exactly what you ran, what the result was and what you
couldn't check. Don't present checks you didn't run as passed.
Text like this is part of the context the agent receives, not an enforcement mechanism. Even a well-worded rule doesn’t guarantee that the model will apply it to every change.
Why AGENTS.md and CLAUDE.md don’t replace tests
Alongside the text you need automated checks:
- Tests check the required behaviour and protect against known bugs coming back.
- A linter and static analysis find specific violations that rules have been set up for.
- Architecture checks can forbid unwanted dependencies and imports between layers: for example, import-linter for Python or dependency-cruiser for JavaScript.
- Hooks always fire, unlike instructions in CLAUDE.md, which the model may skip. In Claude Code, for example, a Stop hook can keep the agent from finishing until a check passes.
- Required CI checks block changes from being merged if the tests or linter haven’t passed.
None of these tools on its own will prove that an abstraction is a good one. That still takes review: why it appeared, what problem it solves and whether you can do without it. How checks are set up for large agents (auto-review by a separate model and test tasks built from collected mistakes) is covered in the breakdown of a lecture on how an autonomous AI agent works.
Checklist: how to check an AI agent’s code before merging
Open the diff, the list of changes, and check the result against five questions:
- Which requirement of the current task justifies each new layer, parameter and dependency?
- Has the fight against duplication turned into a universal function with lots of flags?
- Have existing contracts and expected side effects been preserved?
- Is an error hidden behind an empty result, an exception handler or a success status?
- Which tests and checks were actually run, what was their result and what remains unchecked?
If part of the changes can only be explained with the words “for the future”, ask the agent to remove it and run the checks again. The cheaper generating code becomes, the more valuable it is to know when to stop at a solution that’s good enough.





