Coding agents, defaults, and human judgment
When I worked at Google, some of the engineers who’d been around more of the industry than I had would tell me the codebase was strange.
That didn’t make a ton of sense to me. I found it healthy and pretty intuitive to work in, and the
more experience I’ve picked up since, the more I’ve come to appreciate it — given both the size and
scale of the company and the scope of its products, google3 was in shockingly good shape.
At one point I asked my tech lead where this disconnect came from. What was so weird? His answer was code generation.
Google used code generation extensively at a time when almost no one else did. Even if you were totally sold on the benefits, it was still a new set of tooling to learn, and a different mode of operating than what you might be used to after several years in the industry. If nothing else, you had to rework your muscle memory a bit to reach into the codegen toolbox instead of jumping into writing things by hand, and there was a new set of norms to internalize when tracing code execution in order to find the right declaration sites.
I’d immediately loved the thing other people found frustrating, I’m an unabashed compilers nerd. I like programs that write programs, and I like working on the ones that do. so the speed bump there only made sense to me when he pointed it out.
Before Google I’d been working a lot in Django — which, as frameworks go, isn’t a bad offender for what I’m about to describe. It still bit me more than once. Django is heir to the metaprogramming paradigms that were popular at the time it was built, a key selling point of Rails and everything downstream of it. Extending certain key base classes or adding certain annotations tells the framework to extend your code in various ways and gives you wonderful expressive power and APIs without you having to write very much on your own at all. It’s an ergonomic developer experience, at least 90% of the time.
When there’s a hiccup, though, it gets complicated fast. My experience here is almost certainly
outdated, and I’m sure Django specifically has gotten better, but it was formative for me. Something
in a model wouldn’t behave the way I expected, and finding out why meant climbing into
annotation-declaration code and tracing paths through the framework to work out what was actually
executing. I’d written a short declaration. The code doing the work was somewhere else, and getting
at the actual machine state meant jumping through file after file of library source, working out how
the declarations and overrides all fed into each other. The abstractions I relied on were easy to
ignore until they weren’t, and then I suddenly needed to understand all of them at once.
Google’s generators gave me the same experience on the way in — short, clear, declarative annotations. What they gave me on the way out, though, was much easier to work with — often a single file. When something misbehaved, I could read the generated code in a single place.
That distinction is nice, and it’s one I still use: declarative input, easily inspectable output. Metaprogramming keeps the result in its head. Generation puts it on disk. The difference is invisible while everything works; it decides your afternoon the moment something doesn’t.
Coding agents as code generators
I use coding agents in much the same spirit as I used to use code generators, and it really is the same working mode. I sketch what I need at about the level of detail I’d have put in an annotation, I get code back with the rest filled in, and the result is sitting flat in front of me where I can check it against what I meant. I like that they write code for me. What I like more is that I can open what they wrote, read it, change it, and steer the next pass toward what I actually wanted.
That’s a better answer to “how can you trust generated code” than most of what gets written on the subject. You don’t trust it. You read it. And you can read it, because the output is a real artifact sitting in your repo rather than a transformation happening somewhere you can’t get at.
The analogy breaks in one place, though, and the place it breaks is intrinsically tied up in what makes coding agents powerful.
AutoValue exists because somebody sat down and wrote AutoValue. That was true of every generator
I used at Google. Each one covered a single pattern, somebody had built it deliberately, and the set
of things I could generate was exactly the set of generators that happened to exist. Everything
outside that set I wrote by hand, and most things were outside it.
An agent has no such boundary. Ask for a service skeleton, a migration, a retry wrapper, a test
harness for something nobody has built before, and you get a plausible version of it. Nobody wrote a
generator for that shape. The model already carries a view of what the shape looks like, and that
view does the same job. The opinions are the generator, which is why the range is so much wider
than anything AutoValue could reach.
So most of the time you ride the defaults. You describe roughly what you want, the model supplies a competent version of the rest, and you read the result to see whether it’s what you meant. When it is, you’ve had a generator written for you that nobody had to write. When it misses, you re-steer.
That’s one property doing both jobs. A protobuf compiler has no opinion about how your service should be laid out. It emits what the schema says and goes home. The generators I learned to like were indifferent to their surroundings — same output in a well-organized repo and in a disaster, because the input fully determined the output. That indifference is why they were safe, and it’s also why each one only ever covered the one thing it was built for.
Agents come with a view. About how a service should be structured, where the abstraction boundaries belong, what a test is for. They formed it somewhere else, on someone else’s code, and they’ll apply it to yours anywhere you haven’t made your own view legible.
This is where reading the file in isolation stops being enough. Inspectable output gets you
correctness — you can read what protoc emitted and check it against the schema, the same way you
can read what the agent wrote and check the logic holds and the test covers the branch. What you
can’t get from the file is fit. Nothing in the generated code tells you the schema describes the
right service, and nothing in the agent’s output tells you the thing it built belongs in your
system. That judgment runs on context the agent never had, and you can read every line and still not
have it.
The agent always has an opinion. The only question is whose opinion overrides it.
There’s really only one mechanism at work here. The agent reproduces whatever precedent it can find. What changes is where the precedent came from, and that decides how you re-steer — so two sources are worth separating. The defaults the model arrives with, imposed on you. And the ones it picks up off your own code, inherited from you.
Imposed defaults
Point an agent at an empty repository and it’ll build you something. That something comes out of its training data, because there’s nothing else in the room.
In practice that often means you end up with a slightly odd mix of over- and under-engineering.
You’d think these failure modes are contradictory, but they flow into each other pretty naturally. As an example, agents tend to like introducing abstraction layers before there’s a second implementation to abstract over — clunky, you might think, but benign enough on its own. The issue is that once that excess abstraction is present, it’s easy for the agent to write tests against those abstraction layers rather than against anything the service actually does. You end up with a codebase that looks impressive and a test suite that passes without telling you anything.
One of our researchers ran into the structural version of this. They asked an agent to scaffold a
production service, and it came back with a split-hierarchy monorepo: one directory tree for a
series of independent Python packages, and a sibling tree for worker execution code. Inside that
package tree it pre-emptively split out a core package, a clean place to put common code. It even
generated extensive notes on how the worker instances could be dynamically scaled with KEDA.
On paper, that all sounds great. It missed our actual situation in a few places.
This was supposed to be a single service in our cluster, and the packages wouldn’t be easily
importable anywhere else. The scaffolding had almost as many nested directories in it as there were
files of actual source code — already a red flag — and most of the potential benefits of that
structure were capped by the broader context of our architecture. The service was meant to stay
small, too. Pre-emptively declaring a core package is a judgment call at the best of times, and a
small service has to earn it more than a typical codebase does.
There’s a real benefit to having a structured place for “common” code so that you
don’t end up with an unending list of utils.py files (or the equivalent) that each become a
kitchen sink of functionality and invite code duplication over time. Having an obvious home for
this code gives engineers a cue for where to look before re-declaring a date-to-string formatter
for the umpteenth time. The flip side is that, just as the presence of that obvious home for
common code acts as a behavioral cue to check for pre-existing functionality rather than
re-declaring it, it also acts as a behavioral cue that invites the addition of more “generalized”
function declarations before it’s obvious that they’re actually generally needed. The presence of
a core package can short-circuit
the healthy impulse to write code that’s easy to delete
and is something you should be extra wary of in a service that’s supposed to stay small.
The KEDA notes were completely superfluous too. Our architecture and how the business was actually operating at the time pushed us toward an entirely different approach to performance in this service, and the agent had access to none of that. So we ended up with a thousand words of speculative documentation for something we already knew we’d never do.
The researcher prompting the agent to build this scaffolding didn’t have the background to push back on any of it, though, and that’s where the failure actually sat. The agent did about what agents do — it had seen a lot of mature Python services and produced the skeleton of one. The problem was that the person driving couldn’t tell a good structure from a bad one, or what was appropriate for the task at hand.
The fix for that is an experienced engineer injecting taste into whatever the agent proposes, and it works. Note what it costs, though. Reading the scaffold was free — the researcher could have done it and probably did. Knowing that the scaffold was wrong for us took somebody who already held the architecture in their head, and that’s the scarce thing.
The cheaper version of the same fix is to get that taste into the room before the agent starts. A house layout for a new service, a template worth copying, a paragraph somewhere saying what we do and don’t split into packages — any of it would have given the agent something local to reach for instead of the average of every mature Python service on the internet. It’s the same judgment either way. The difference is whether an expert spends it once, in a form other people and other agents can pick up, or spends it again on every review.
The review-shaped version has a ceiling, and the ceiling is organizational. Nobody has experience with every part of what you’re building, so at some point the defaults land where nobody qualified is looking, and you find out in hindsight. That turns out to be the expensive part, and I’ll write about that more later on.
It’s a large part of why we settled on researchers building prototypes and applied engineering building the production services — an understanding rather than a written policy, but one that agents made more important rather than less.
Inherited defaults
Now put something else in the room. Give an agent a repository that has history, and the agent will read that history as the specification for what good looks like here. That’s actually what a codebase always is, in a way; the problem is that agents lack a certain level of distrust and instinctive skepticism that helps humans slow down around a questionable section of code, rather than freely replicate whatever that section happens to carry.
An example of what this can look like in practice:
We had various services talking to each other over gRPC. During initial development, engineers would
bind them to non-standard ports, off :50051, so that two servers running on the same laptop
wouldn’t collide. Sensible. Then an engineer who hadn’t spent much time with port mechanics baked a
couple of those development ports straight into Dockerfiles instead of handling the mapping in
Docker Compose. Then the agents saw that this was a plausible pattern for future services, and the
issue started to replicate. We ended up with built services listening mostly on :50051, except
for the ones that listened on :50053, or :50054, or :50057 — a network topology that made no
sense to anyone reading it fresh, and a cleanup we had to come back for later.
:50051 is the conventional gRPC port. Nothing in the stack objects to
:50053 — it just quietly means something different to everyone who reads it afterward.
There were similar issues in parts of our frontend code, but the problem grew worse because it went on longer. It was the area where our initial team had the least depth, and it accumulated the kind of debt that shows up when nobody in the room has strong opinions: many JSX components in a single file, or complex data representations awkwardly passed between components directly rather than hoisted into context layers. During one deliverable crunch, an engineer added some additional GraphQL queries on page load to a couple of screens, making a mental note to come back and combine them with the primary page-load query if they turned out to still be needed. It meant two round-trips on initial load for a few screens, but that’s the kind of thing that can be tolerable for a couple weeks when the second query might get deleted entirely.
The problem, of course, is that the agents read all of that and then produced more of it, faithfully. Those long files that felt a bit bloated — bloated, but not quite bad enough that it felt pressing to split them up on the spot — got appended to indefinitely. A component prop that might have been better as a context value was joined by several siblings just like it. And queries on page load kept multiplying until we had three or four blocking the render of certain screens and the extra half-second of latency drew attention to itself. That led to a painful series of cleanups — first targeting specific prototype UIs that were growing in unhealthy ways, then a broader review of the frontend codebase to root out the patterns that were proliferating.
Nothing failed in either case. No test went red, no build broke, nothing threw in production. The tooling reported success at every step, and against every check we had, it was successful. The code was correct. It was just wrong for us, and nothing we had was looking for that. Both got caught by people — reading the surrounding code while working on something else, and noticing something was off.
In both cases the fix had to come in at least two pieces. One was addressing the immediate problem:
an erroneous port assignment, a poorly handled bit of UI code. The other was correcting the pattern
the agents had been replicating. On that second piece, a practical way forward is usually a first
pass of refactoring combined with additional agent cueing — checked-in documentation, SKILL.md
files, that sort of thing — to staunch the bleeding as fast as you can. Depending on how much debt
you’ve racked up in an area, cleaning everything up in one go isn’t always feasible; sometimes you
do what you can and lean on the tooling to keep things from getting much worse.
Layered-on tooling is inherently more fragile than a healthy codebase the LLMs can extend naturally, though. “Context rot” — agents dropping instructions when they’re overloaded with specifications — is real and painful here. So prioritize going back and cleaning up the problematic patterns themselves, to keep them from creeping back in later.
Where’s the upside?
That’s two sections of complaints, which makes it fair to ask whether these things are worth the trouble at all. My answer is that they are, and not marginally — over the period I’m describing, the agents were a substantial net gain to my team’s development, and I’d make the same call again.
I do want to take some care in how I’m presenting that, though. None of it is measured. I didn’t run a counterfactual, and I can’t cleanly separate the agents from everything else that changed over the same period — the platform got more extensible, and we got better at understanding the problem space. Self-reported speedups are also exactly the kind of thing people get wrong, and I have no particular reason to think I’m better calibrated than anyone else. So what follows is what the work looked like, not a measurement of what it was worth, and it’s something I’ll probably come back to.
Contract MVPs got fast. Nine working days from the first requirements conversation with a customer to a live demo, on a product that needed both a new operations-research model and a new UI. Both were close to things we’d built before; the code implementing them was still maybe 75% net new. The customer’s verbatim feedback was “you guys figured something out in 2 weeks that we’ve been unable to solve in a year of working on it.” Some of that was platform work that made the underlying system more extensible, and some was us understanding the problem space better than we had before. Some of it was shipping a lot of code in a short window.
I built the initial form of our authorization system in about two weeks. That’s the one I trust most, because it’s one of the areas where I’m closest to being an expert. I’ve worked with and on a lot of authorization systems, including two years entirely focused on the authz problem-space at Google as part of the Drive sharing team. We owned permissions for Drive and everything built on top of it — Docs, Sheets, the rest — and I learned a tremendous amount from the truly world-class experts who were there. I scoped the initial implementation tightly and stayed hands-on steering the agent, but I still can’t construct a hypothetical world where I write that system by hand in under three or four weeks. The caveat above applies hardest right here, though: that’s a counterfactual nobody ran, estimated by the person with the most invested in the answer. On the other hand, the biggest wins were less about raw speed than about the things I knew should be there and would have been tempted to cut in an MVP crunch: seams for functionality we hadn’t built yet, unit tests that both checked correctness and pinned certain essential behaviors.
Gateway services stopped being a thing we deferred. We have a few that abstract external dependencies needing per-environment configuration — one fronting LLM providers so the stack can hit either Anthropic or OpenAI directly, or go through cloud-provided services like Bedrock; one for blob storage so it can sit on S3, Azure Blob Storage, MinIO, or whatever else. They’re easy to put off, because each needs real service scaffolding before it does anything useful: API endpoints, server lifecycle, the rest. Agents made them cheap to stand up, and made adding a new provider to an existing gateway close to trivial. That second part matters more than it sounds. Most of the work in per-provider configuration is mapping a common idea onto whatever special-snowflake framing a given vendor picked for it, and most of the wall time goes to reading their documentation to find out what that framing is. Coding agents can cut down the toil there dramatically, freeing you up as an engineer to focus on any more meaningful architectural differences you might need to account for rather than the specific magic incantations any given SDK needs.
An entire category of internal tooling stopped being necessary. Our researchers get a lot of use out of having an LLM build them one-off visualizations for exploring a dataset or a problem space. That work used to be gated behind dedicated frontend engineers building internal products, plus the planning to make those products flexible enough for needs nobody had articulated yet. Generating the visualization on demand removed the need for the whole category. That’s a win twice — once in immediate velocity, once in the maintenance burden the team never picked up.
All of that is my own experience, which is the weakest kind of evidence there is. The strongest
public evidence I know of points the same way, though, and it lands in the population most distinct
from my recent time in startups. Google’s Big Sleep, a Project Zero and DeepMind collaboration,
found an exploitable stack buffer underflow in SQLite in late 2024 — caught before it reached a
release. After the fact, the team went back and tried to rediscover the same bug with AFL, having
first confirmed the fuzzing corpus contained the keywords needed to reach it, and came up empty
after 150 CPU-hours. Anthropic’s Frontier Red Team has since been pointing Claude at open-source
projects with no custom harness at all, and finding high-severity memory-corruption bugs in code
that had been fuzzed for years, some of them undetected for decades.
The Big Sleep team,
From Naptime to Big Sleep: Using Large Language Models To Catch Vulnerabilities In Real-World Code,
Google Project Zero, November 2024 — the SQLite find and the 150 CPU-hours of failed AFL
rediscovery are both from that post, which also explains why the existing fuzzers missed it:
OSS-Fuzz’s harness wasn’t built with the generate_series extension enabled, and the
alternative harness carried an older, unaffected version of the function. So this was less a bug
that survived years of fuzzing than one the fuzzers were never pointed at. Read their conclusion
too, because they undercut themselves harder than any critic would: “at present, it’s likely
that a target-specific fuzzer would be at least as effective.” Anthropic’s side is Nicholas
Carlini et al.,
Evaluating and mitigating the growing risk of LLM-discovered 0-days,
Frontier Red Team, February 2026, with the funnel and severity-agreement figures from their
coordinated vulnerability disclosure dashboard, snapshot
dated 22 May 2026. Both are labs grading their own models, and both should be read with that in
mind. I’m citing them because the disclosure counts are auditable and because both publish the
places the model was wrong — not on the strength of a vendor saying its model is good.
Read the funnel rather than the headline, though, because the funnel is the more interesting number. Anthropic’s disclosure dashboard, as of May 2026: 23,019 candidate findings, of which 1,900 had been through review by outside security firms, at a 90.8% true-positive rate. Only 467 of those reached a maintainer that way. Another 1,129 went straight to maintainers untriaged, at the maintainers’ own request, for 1,596 disclosed across 281 projects and 97 patched upstream. Anthropic’s own explanation for that shape is that independent human triage is the rate-limiting step. And the model’s severity ratings matched the external reviewers exactly 58.7% of the time — when they diverged, it was usually the model grading its own find too high.
Which is the same shape as everything above. Finding the bug got cheap. Deciding whether it matters is still the expensive part, and it still runs on somebody’s judgment.
Looking for research about where I’m right and wrong
So that’s my case for the upside. The best-designed study anybody has run disagrees with me.
METR found that AI made developers slower. Joel Becker, Nate Rush, Elizabeth Barnes and David Rein, Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity, arXiv:2507.09089, July 2025 (doi:10.48550/arXiv.2507.09089). Worth reading in full — the paper evaluates twenty candidate explanations for the slowdown and rules most of them out, and Appendix B lists the overgeneralizations the authors explicitly don’t endorse. The February 2026 follow-up is We are Changing our Developer Productivity Experiment Design. Sixteen experienced open-source developers, 246 real issues drawn from their own projects, each one randomly assigned to allow or disallow AI tooling. Allowing it increased completion time by 19%. The developers had forecast a 24% speedup going in, and after finishing they still believed they’d gotten a 20% speedup, despite being empirically slower on average.
That gap between the measurement and the feeling is the most useful thing in the paper, and I’d apply it to my own impressions before anybody else’s. Start with the two weeks I just claimed for the authorization system. The thing I offered as the reason to trust that number — that I know these kinds of systems well — is the property those sixteen developers all shared. They were working in the repositories they knew best, and it didn’t protect them.
But I don’t think the result fully contradicts the benefits I saw, and neither does METR. The repositories studied were the largest and most familiar ones those developers work in — averaging over a million lines and twenty-two thousand stars, on projects they’d been contributing to for years. METR’s own list of things the study does not show includes this one: “We do not claim that our developers or repositories represent a majority or plurality of software development work.” They also note that AI capabilities “may be comparatively lower in settings with very high quality standards, or with many implicit requirements.”
That’s a specific kind of codebase, and it’s worth being careful about what makes it one. “The agents are bad in codebases that need a lot of context” doesn’t discriminate — every codebase runs on context that lives in somebody’s head. Mine certainly did.
What changes between them is the kind of context. On a greenfield service, the load-bearing part is what you’re trying to prove and what business signal you’re trying to collect, and that fits in a few paragraphs, which means it fits in a prompt. On a million-line system, it’s how this thing talks to six others, which of those lie about their error codes, and the failure mode somebody learned about during an outage two years ago. None of that compresses, and most of it was never written down anywhere an agent can reach.
Mine sat on the greenfield side of that line, though I would note that they were still “production” services by every definition that matters — real users, real money, real uptime, just young enough that most of the work was still net-new.
METR has been pursuing the legacy-versus-greenfield thread itself. Its February 2026 follow-up ran a second study across 57 developers and 143 repositories, with the newly recruited majority drawn deliberately from “smaller, more greenfield, and less mature repositories.” The ten developers returning from the first study came out at an 18% speedup, the new ones at 4%, though both confidence intervals are wide enough to cross zero. METR reads those as a lower bound, because developers who most wanted to use AI declined to take part and the tasks with the highest expected uplift never got submitted. The write-up is titled “We are Changing our Developer Productivity Experiment Design,” and its conclusion is that the design has stopped producing a reliable signal — they’ve marked the original 19% slowdown as out of date without claiming a replacement for it.
I’m actually skeptical that the legacy-versus-greenfield division is deeply explanatory, though. The vulnerability results I mentioned earlier land in exactly the same population METR sampled. Old code, enormous code, fuzzed for years, maintained by people who know it intimately. That’s the profile the study says agents struggle in, and it’s the profile where they turned up bugs nobody had found in two decades. Both results look sound to me. I don’t have a story that dissolves the tension, and I’m slightly suspicious of the instinct inside myself to try to settle these things neatly when we probably just need a lot more empirical evidence and testing of what works and what doesn’t. One of the challenges of where we’re currently at is that there just hasn’t been a ton of high-quality, credible research published on coding with LLMs yet. I described the METR study as “the best-designed study anybody has run”. That is true, as far as I can tell, but it’s also true that it’s winning that race by default in some ways, which invites all kinds of pushback — including mine, here. Some of that is just a matter of timing: working a paper through the peer-review process alone takes a few months, so designing strong studies on emerging tooling and getting them published is going to have inevitable lag time. The long-standing struggles of measuring engineering productivity make this issue even harder to study, though, so those of us on the practitioner side of things might be waiting a fair while longer to get clear signal from our friends in academia, even as we get bombarded by findings pushed forward by companies with various commercial agendas we should be evaluating carefully.
The closest I get to closing the loop in my own head is to say that the implementation METR studied and the cybersecurity vulnerability patching are different kinds of work, along the same line I drew earlier between correctness and fit. A memory-safety bug in SQLite is a correctness problem, and a bounded one — the bug is a bug whatever the maintainers were trying to build, and finding it takes hard reasoning about a small region of code rather than the whole system. Shipping a feature into a million-line service is a fit problem, and fit needs everything the file doesn’t contain. Agents look strong on the first and weak on the second. That’s a real distinction, though I still don’t think it’s the whole answer.
There’s a third possibility I’d hold alongside the other two, and it’s the one the study design can’t see. Completion time on an assigned task assumes the task holds still. My experience is that it grows. The authorization work is the clearest case I have, and the wins I described there were seams and behavior-pinning tests rather than hours saved — the things I’d have quietly dropped under time pressure. Same task, same clock, a better artifact at the end of it. A stopwatch records that as a tie. It’s also most of why I generally tend towards being relaxed about the future of software engineering as a profession: new capability has a way of raising what counts as “done” rather than reducing how much there is to do.
What the investment buys
There’s one more explanation I’d give real weight, and it’s the one that ties back to the generators. Getting real leverage out of these tools takes investment — both in learning the tools and in the codebase itself — and that investment takes a while to pay back. Someone who already knows a system cold, working a task squarely inside their own expertise, is plausibly just faster doing it by hand. METR raises a version of this: the study’s developers had a few dozen hours on Cursor, and there may be learning effects that only show up after several hundred.
The generators I opened this piece with worked the same way. Writing out a constructor and a set of
getters by hand never took me meaningfully longer than writing the AutoValue annotations that
generate them, and when I knew the exact shape I wanted I could come out ahead by skipping what the
generator insists on including. Measured one class at a time, codegen (probably) loses.
What it definitely bought was consistency — every value type in the codebase the same shape, so
the next person could read one and infer the rest — and a row of footguns disarmed before anybody
got near them. A correct equals and hashCode pair is exactly the kind of thing that gets skipped
when you’re in a hurry. Then, once you trust it, it stops occupying any room in your head. The
savings were real and they were aggregate: across any given engineer’s year, and much more across
everyone else’s.
Working with agents has the same shape, which is one reason a careful task-level measurement can be right about the task and still miss the year.
A large part of making a codebase work well with agents comes down to some variation on writing out what you actually want — the structure, the conventions, the things you’d otherwise have said in a review comment. That work is what makes your own defaults legible enough to win.
That focus on a specific kind of legibility can feel burdensome at first, especially when it’s spent
making things obvious for coding agents that an experienced human engineer would have picked up on
their own. What makes me feel better about it is that the documentation helps the humans who come
after you too. A SKILL.md file on generating a database migration can be critical for an agent,
and it’s also a useful clue for the next person about how you were managing database state as you
iterated on a product. The fact that it has to be checked in to work properly is a nice property
here, and it’s much less likely to go silently stale, if only because the developer who wrote it
will be annoyed when the agent starts drifting on them.
What to do about it
Writing out what you want is a very general prescription, though, and I don’t want to end on it without anything concrete attached.
Start by figuring out whether a bad bit of agent-generated code was copied or invented. That’s the one mechanism from earlier, run backwards — you’re asking which source the precedent came from, because the answer picks the fix, and the two look identical on the surface. If the agent inherited the pattern, the fix is upstream: clean the source it learned from, with written prohibitions as a fallback. If the agent invented the pattern, cleanup buys you nothing, because there was nothing to clean. You have to hand it a structure to follow.
Your worst code is now your highest-leverage cleanup target. The module nobody has opened in a year is a training example, and it’ll go on being one every time an agent goes looking for local precedent. Context rot means coding agents are much worse than humans at ignoring the unhealthy or legacy sections of your codebase; it all ends up being part of your baseline, and you’ll want to take it on as aggressively as you can.
The corollary is the part I’d argue hardest for, because it cuts against what most teams reach for
first. Writing the prohibition down is a stopgap. A SKILL.md file telling an agent to avoid what
your codebase demonstrates on every page is one instruction competing against a pile of evidence,
and context rot decides that contest — the more you pile on, the more reliably some of it gets
dropped, and it goes quiet when it happens rather than failing loudly. Treat the documentation as
scaffolding around a cleanup that’s still moving, and keep the cleanup moving.
Spread the judgment, don’t just apply it. The biggest challenge dealing with issues from what I
called “imposed defaults” is that the ceiling is organizational. One engineer with taste catching
one bad scaffold fixes one bad scaffold; the next one lands in front of somebody who still can’t
tell, and the ceiling hasn’t moved. What raises it is getting that judgment out of one person’s head
and into places other people will hit it — the review conversation, the team channel, whatever
passes for institutional memory where you work. Write it down for the agents too, in a
lessons-learned folder or similar, so the next agent that goes looking for precedent finds the
reasoning and not just the corrected code. But treat the socializing as the point and the file as
the artifact, rather than the other way around.
A lot of the worst pains of agentic coding come from agents amplifying an existing tendency for engineers not to talk to each other.
Where I’m still unsure
I’d rather not overclaim any of this. It’s a moving target, and some of what I’ve written is already less true than it was a year ago.
The agents improved at a fair amount of this on their own, while we were busy building tooling to work around them, and I can’t cleanly separate the two. We were getting better at using them at the same time they were getting better. That leaves attribution on any specific complaint fuzzy. Which of that tooling was permanent, and which was me patching something the next model fixed for free, is a question I’m still working through — and something I’ll also probably write more about later.
I also touched on the delay between investment and payoff earlier. I think that delay is most of why this stuff is contentious. Every engineer I know has burned a week on a library that was going to fix everything and left them roughly where they started, though I’d hesitate to call that time wasted — you come out knowing where the edges are. But it does mean two reasonable people can hold opposite views for a long stretch before anything settles it.
Somebody has to make the call anyway. In most engineering organizations somebody in a leadership position picks which bets the team is making, and everyone else commits to that until the evidence arrives. That’s a real cost, and it’s paid by the people who didn’t get their way. I think it’s where most of the heat in these arguments actually comes from. The technical disagreement is usually narrow; what stings is being asked to commit to somebody else’s bet without feeling heard on it.
There’s also no disaster attached to any of my experience, which, weirdly, makes the opportunity for disagreement worse. No incident, no rewrite, nothing I’d file as a catastrophe. Every one of the failures above got caught and cleaned up. That’s part of why the argument stays open — the expensive failures in this line of work don’t arrive as outages, so there’s never a moment that settles it for anybody. They arrive as a codebase that drifts somewhere slightly wrong and looks completely fine while it does.
So I read what the agents write. That habit’s the one part of this I’m definitely sure about. The rest of it — the re-steering, the tooling around it — I expect to keep rewriting.