My agent opened a PR: +214 / -3. The task was "remove the legacy retry path."
It did not remove the legacy retry path. It wrote a new retry path, put the old one behind a flag that defaults to on, added a compatibility shim between them, and deleted three lines of comments. Tests passed. The code was, honestly, fine code. There was just more of it than before, and I had asked for less.
That is the pattern I keep hitting, across models and across harnesses: AI coding agents never delete code. They add. Then they add a wrapper around what they added. Then, when you say "simplify this," they add a simplification layer.
I got curious and ran git log --numstat over six months of my main side project, splitting commits by author. My hand-written commits sat around 3 lines added per line removed. The agent-authored ones were north of 10:1. Same repo, same feature work, same me reviewing. The difference was not skill. It was direction: the agent only knows how to go one way.
TL;DR
- AI coding agents never delete code because deletion is a whole-repo claim ("nothing else uses this") and the agent only sees the handful of files it happened to open.
- Adding is locally verifiable: write code, run tests, green. Deleting is globally verifiable: prove no caller anywhere breaks. Agents optimize for the thing they can check.
- The bloat has a signature: parallel helpers, old paths kept behind flags, defensive branches for impossible states, and commented-out tombstones.
- Fix it by making deletion the task, not a side effect. Separate subtraction PRs, hand the agent the grep evidence, and require a negative net diff.
- If you want it to stick, ratchet it in CI. A failing check works; a line in CLAUDE.md does not.
Why do AI coding agents never delete code?
Because deleting code is a claim about the entire repository, and the agent is working from a keyhole view of it.
Three things stack up:
1. Verification is asymmetric. When an agent adds a function, the proof it worked is right there: the test it just wrote goes green. When it removes a function, the proof is the absence of breakage everywhere, including the template file it never opened, the dynamic import, the string-keyed dispatch table, the cron job. The agent cannot run that proof. So it hedges.
2. Context is partial by construction. Your agent read 12 files. Your repo has 900. It has no idea that formatCurrency already exists in lib/format.ts, because it grepped utils/ and found nothing. Adding is safe under uncertainty. Deleting under uncertainty is how you page someone at 2am.
3. Doing more looks like helping. I can't see anyone's training data, but the behavior is consistent with a strong prior that a thorough answer is a long answer. Ask a model to clean up a file and reply with a 40-line deletion and nothing else, and it reads as lazy. Reply with a refactor, a docstring, a type guard, and a comment explaining the deletion you didn't do, and it reads as diligent. One of those is right and it is not the one that gets rewarded.
Put those together and you get an agent that treats your codebase like a whiteboard nobody is allowed to erase.
What does agent-written bloat actually look like?
It has a fingerprint. Once you can see it, you cannot unsee it in a review.
The parallel helper. A second slugify, a second retryWithBackoff, a second date formatter, each slightly different in the edge cases. Nothing is broken. Six months later a bug fix lands in one of the three and not the others.
The flag that defaults to old. "I've kept the previous implementation behind USE_LEGACY_SYNC for safety." Now you own two code paths forever, and one of them is untested in production and untested in CI because nobody flips it.
Defensive code for states that cannot happen. if (!user) return null inside a function that only runs behind auth middleware. Individually harmless. A hundred of them and your codebase turns to fog, because now every null check is ambiguous: is this a real case or agent lint? Worse, those branches swallow the bugs that would have told you something upstream is broken.
Comment tombstones. // Previously handled by processQueue(), see PR #412. Commented-out blocks "kept for reference." Your VCS already does this. The agent does not trust that you have one.
Isn't this just bad prompting?
Partly, yes. Prompting moves the needle and you should do it. "Delete the legacy retry path. Do not write a replacement. The new path in sync/retry.ts already covers every caller" gets you dramatically closer than "clean up the retry logic."
But it decays. Put "prefer minimal diffs" in your system prompt and watch it hold for the first two tool calls of a long agent loop, then evaporate somewhere around step 15 when the model is deep in its own context and optimizing for closing the loop. Instructions are weak forces. Structure is a strong one.
The honest version: prompting fixes maybe a third of this. The rest is on you for asking an agent with a keyhole view to make a whole-repo decision without giving it the whole-repo evidence.
How do you make an AI coding agent actually delete code?
Give it what it is missing: evidence, permission, and a hard constraint.
1. Split the subtraction pass from the addition pass. Never in the same PR. Feature PR ships the new path. A separate PR, a separate agent run, does one thing: remove the old one. When deletion is the only success criterion, the model stops hedging, because hedging is now visibly failing the task.
2. Hand it the call-graph evidence up front. Do not ask "is this used?" Run the search yourself and paste the result:
rg -n --stats 'legacyRetry|LEGACY_RETRY' -g '!node_modules' -g '!dist'
rg -n 'legacyRetry' -g '*.{html,ejs,yaml,yml,json,sql}' # the ones it will forget
Dynamic dispatch, config files, and templates are exactly where an agent's confidence outruns its knowledge. I have had an agent declare a function dead because it only searched .ts files. It was called from a Handlebars template. Provide the evidence, or verify the claim yourself before you accept it.
3. Make the constraint numeric. "The net line count of this diff must be negative" is a constraint a model can actually evaluate against its own output. "Be concise" is not.
4. Tests are the oracle, and the agent must run them. Not "this should be safe to remove." Run the suite, paste the output. Claimed-green and actually-green are different states of the world.
5. Ratchet it in CI. This is the part that survives contact with a busy week.
# .github/workflows/diet.sh
ADD=$(git diff --numstat origin/main... | awk '{a+=$1} END {print a+0}')
DEL=$(git diff --numstat origin/main... | awk '{d+=$2} END {print d+0}')
if [ "$ADD" -gt $(( DEL * 4 + 200 )) ]; then
echo "Diff is $ADD added / $DEL removed. Justify with the 'growth' label or cut it."
exit 1
fi
Crude on purpose. It is not measuring quality, it is creating friction in the one direction that currently has none. Add a label escape hatch for genuinely new features and let it annoy you the rest of the time.
What does it cost you to ignore this?
It compounds, and it compounds against the agent itself.
Next week that agent reads the codebase it wrote. Three near-identical helpers means retrieval pulls the wrong one. Two flag-gated paths means it has to reason about both. Fog-level null checks mean it cannot tell which invariants are real. You pay in tokens, in latency, and in the agent making worse decisions because its own past output is the noise in its context.
The trap is that none of this shows up as a bug. It shows up as your agent getting slowly, unaccountably dumber in your repo while it stays sharp in a fresh one. That is not model drift. That is the landfill you let it build.
So, why do AI coding agents never delete code?
Because adding code is a local action an agent can verify on the spot, while deleting code is a global claim it has no way to prove from inside a partial view of your repository, and under uncertainty the safe move is always to add. The behavior is rational given what the agent can see. The fix is not a better model or a sterner prompt, it is structure: run deletion as its own task with its own PR, hand the agent the search results that prove a symbol is unused, require a negative net diff, make it run the tests instead of predicting them, and put a lines-added ratchet in CI so growth costs something. Do that and agents delete code just fine. They were never refusing. They were just never asked in a way they could answer.
Top comments (0)