Step 0: delete dead code before you refactor (or the LLM will get lost)
Every byte you load into an LLM agent's context is a byte it has to keep track of. Dead code looks like signal — and agents preserve, extend, or mimic it. Our rule: a pure-deletion commit before any structural refactor of a file over 300 LOC.
AppX team ·
Every time you hand an LLM coding agent a file, you are loading bytes into a finite context window. Each byte costs attention — not in the abstract "tokens are expensive" sense, but in the concrete "the model now has to decide whether this line matters" sense. Dead code is the worst kind of byte. It looks like signal. It reads like a real function. The agent can't tell that the surrounding team has known it's garbage for six months.
Our rule: before any structural refactor of a file larger than ~300 LOC, you do a Step 0 commit that does nothing but delete dead code. No restructure. No new feature. No "while I'm in here" rename. Just delete. Commit. Then start the real refactor against a clean tree.
This sounds like fussy hygiene. It is not. It is the single highest-leverage move when working with Claude Code, Cursor, aider, or any other LLM agent on a non-trivial file.
Why dead code makes agents stupider
An LLM doesn't have a "trust score" for the code you show it. If a function is exported, the agent assumes someone imports it. If a prop is in the type, the agent assumes something passes it. If a comment says // TODO: replace with v2, the agent will helpfully replace it with v2 — even if v2 has been live for a year and the TODO is the fossil.
The pathologies are predictable once you start watching for them:
- Preservation by default. The agent leaves dead branches alone "to be safe." Your 600-line file stays 600 lines after a refactor that should have produced 250.
- Phantom call sites. The agent extends
legacyFoo()because it's right there in the file, instead of using thefoo()that replaced it. Now you have two call paths to maintain. - Pattern mimicry. The agent sees three commented-out
useEffectblocks and writes its new effect in the same broken style, because that's the "house style" the file taught it. - Re-implementation. The agent re-creates a helper that already exists thirty lines below the cursor, because the existing one is wrapped in a
/* deprecated */block it learned to skim past.
Concrete example. We had a component with this near the top:
// TODO: remove after v2 cutover (Q3)
function legacyNormalizeUser(u: RawUser): User {
return { ...u, displayName: u.name ?? u.email };
}
The v2 cutover shipped. Nobody deleted the function. Six months later, an agent asked to "add a tenantId field to the user normalizer" dutifully extended legacyNormalizeUser — not the real normalizeUser that lives in a sibling file and is the actual production path. The PR passed review because the diff looked plausible in isolation. It shipped to staging before anyone noticed normalization was now happening in two places with two different shapes.
The agent did exactly what the file told it to do. The file lied.
Step 0 in practice
The mechanics are boring, which is the point. You want this to be a five-minute reflex, not a project.
- Run
ts-pruneorknipagainst the package. Both will list unused exports, unused files, and unused dependencies.knipis more aggressive and catches dead test files and dead config too. - Turn on
eslint'sno-unused-vars,no-unused-imports, and (if you have it)@typescript-eslint/no-unused-varswithargsIgnorePattern: '^_'. Runeslint . --quietand sweep. git grepfor the dead-comment dialects your team uses:// TODO removed,// deprecated,// legacy,// kill this,// XXX. Each one is a candidate.- Look for typed props that no caller passes. TypeScript won't always flag these — an optional prop is "used" as long as the type lists it.
- Delete commented-out code. All of it. If you need it, it's in git.
Then — and this is the load-bearing part — commit only the deletions. A single commit titled chore: step 0 — delete dead code in <file> with a diff that is 100% red. No refactor mixed in. No "small fixes." Pure subtraction.
The reason it must be its own commit is rollback. The refactor that follows is the risky operation. If it goes sideways and you git revert it, you want the dead-code deletion to survive. Mixing them means the revert resurrects the corpses, and now the next agent attempt walks straight into the same trap.
When you skip Step 0
It is not a universal law. Skip it for:
- Files under ~150 LOC where you can see the whole thing on one screen.
- Throwaway scripts, one-off migrations, and anything in
scratch/. - Generated code. Don't hand-edit the generator's output; fix the generator.
- Files you're about to delete entirely. No point grooming a corpse.
The threshold is whether the agent can hold the file in its head clearly. Below ~300 LOC, dead code is annoying but survivable. Above that, it compounds.
The harder cousins
Dead code is the obvious case. The same rule applies, with less tooling support, to:
- Dead CSS. Selectors that match nothing. Agents will happily extend them. Tools:
purgecss,unused-css, or a quick "delete it and see what breaks visually." - Dead docs. README sections describing flags that no longer exist, architecture diagrams from two rewrites ago. The agent will read these as ground truth.
- Dead tests.
it.skipblocks that have been skipped for a year.describe.onlysomeone forgot. Test fixtures referencing removed endpoints. Agents pattern-match on tests heavily — bad tests teach bad patterns.
Same Step 0 rule: prune first, in its own commit, before you ask the agent to touch the surrounding live code.
Takeaways
- Dead code isn't neutral when an LLM reads it. It actively misleads. Delete before you refactor.
- Step 0 = pure subtraction, isolated commit, no mixed concerns. Rollback safety is the whole point.
- Lean on
ts-prune,knip, and strict eslint rules — they find more than you will by eye. - Apply the same rule to dead CSS, dead docs, and stale tests. Anything in the agent's view counts.
- If the file is small enough that you can see all of it at once, skip Step 0 and just work. The rule is about scale, not ceremony.