Why Your Legacy React Codebase Confuses AI Coding Agents
AI agents stumble on mature codebases because they have accreted multiple valid answers to the same question — and an agent has no way to know which one is current.
I spent a week auditing a mature React frontend — five years old, ~30 page modules, MUI v5, mid-migration from Redux to Zustand and from SCSS to Emotion. Healthy by every traditional measure: ships features weekly, passes type-check, has a real test suite, decent docs.
It also confuses AI coding agents on the very first file.
Not because the agents are bad. Because the codebase has accreted multiple valid answers to the same question, and an agent — unlike a senior engineer — has no way to know which answer is the current one.
This post is about why that happens, and how to recognise it in your own codebase before you write a single rule for Claude Code, Cursor, or Copilot.
The symptom: “Claude wrote it differently this time”
You set up Claude Code or Cursor with a good CLAUDE.md / .cursor/rules/. You write a task description. The output is reasonable. You run it again on a similar task — and the output uses a different component, a different state pattern, a different import path.
Same prompt. Same rules. Different answer.
The natural reaction is to write more rules. Don’t. More rules rarely fix this. The agent isn’t ignoring the rules; the rules are contradicting nearby code, and the agent is splitting the difference each time.
Here’s what’s actually happening.
What an AI agent sees when it opens your repo
A human developer working in your codebase has organisational memory:
“Oh,
components/__v2__/was the second attempt at the design system. We moved toatoms//molecules/in 2024 but didn’t migrate__v2__because it’s expensive. Don’t touch it. New work goes inmolecules/.”
You know this. Your team knows this. None of it is written down anywhere a model can find.
An agent has exactly three inputs:
- The rules document (
CLAUDE.md,AGENTS.md,.cursor/rules/). - The files it reads at runtime (the surrounding code in the directory it’s editing).
- Statistical priors from training data (the average React app on GitHub).
When (1) contradicts (2), the agent has to choose. Most of the time it picks (2) — the example in the nearest file — because that example is concrete and the rule is abstract. The result is consistent-with-the-neighbours, but inconsistent with your stated direction.
Run the task again with a different file as the agent’s starting point, and it picks a different neighbour. New “correct” answer.
The five patterns that confuse agents most
In the codebase I audited, I measured these against the documented rules:
| Stated rule | Actual count |
|---|---|
”No @ts-ignore / @ts-nocheck” | 3,141 in committed code |
”No barrel imports from components/atoms” | 714 violations |
| ”No code in deprecated dirs” | 3 deprecated dirs, actively imported |
| ”One canonical theme per module” | 30 modules, 55 theme files |
| ”No hex literals — use design tokens” | Theme file itself contains literal hex |
Each row is an agent’s nightmare: the rule and the code disagree, and they disagree thousands of times per repo. The agent reads ten files in the area it’s editing, sees ts-ignores in nine of them, and concludes “ts-ignore is normal here.” It then writes one too.
The fix is not “tell the agent harder.” The fix is to remove the contradiction.
Pattern 1: Multiple parallel component hierarchies
Look at your components/ directory right now. How many of these do you have?
atoms//molecules//organisms/__v2__/,legacy/,old/,deprecated/- A
forms/orform/dir (often older) - A handful of
.tsxfiles at the root (Drawer.tsx,Select.tsx,Tabs.tsx) - Feature-specific dirs at the top level (
Charts/,Widget/,MyTable/)
Three or more of these and your agent has no canonical answer to “where do I put a button?”. I’ve seen codebases with five parallel hierarchies in components/, each containing its own Drawer, Select, and Modal. Every one of them is a from 'components/Drawer' away from being imported.
Pattern 2: Form fields scattered across four directories
In one repo I audited:
FormTextFieldinmolecules/and in__v5__/FormCheckboxinmolecules/andFormCheckBox(different casing!) in__v5__/FormSelectFieldonly in__v5__/— banned dir, but the only implementation- Radio buttons in
atoms/, radio groups inmolecules/ - Form-control primitive deep in
components/base/
The migration guide told agents “use molecules” and then directly instructed imports from __v5__/FormSelectField because there was no canonical replacement. The repo’s authoritative agent prompt was pointing at the dir the rules banned.
Pattern 3: Design tokens that mean the opposite of their name
Real example from colors.ts:
secondary: {
main: '#F19100', // amber
dark: '#FBC064', // LIGHTER than main
light: '#C17400', // DARKER than main
}secondary.dark is lighter than secondary.main. secondary.light is darker. An agent reading “use the dark variant for hover” will produce the wrong colour every single time, and no rule can save it because the token name itself is misleading.
Same file had secondary.skyBlue (#00A3E9) — a blue colour in the amber namespace — and custom2.background: '#F44336' (bright red) in a namespace called “custom orange”.
When the type system can’t tell you the colour is wrong, no agent can either.
Pattern 4: Two state libraries, two styling systems, no migration doc
The architecture doc lists Redux as a first-class option with examples. The CLAUDE.md says “client state: Zustand.” Reality: both are wired up across pages. The two docs contradict each other, and neither says “Redux is legacy, do not add, do not delete.”
An agent reading the docs concludes both are valid. An agent reading nearby code does the same. The codebase keeps growing in two directions at once.
Pattern 5: Migration guides that reference banned territory
This is the most painful one because the migration guides are usually the best content in the repo — clear, opinionated, code-templated. But when the guide says “use molecules/X” and X doesn’t exist in molecules/ yet, the guide quietly redirects to the banned dir.
The guide is now self-contradicting. The rules don’t work.
Why “just write better rules” fails
I’ve seen teams react to agent inconsistency by writing longer CLAUDE.md files. 800 lines. 1,500 lines. Every edge case enumerated.
The longer the rules, the less consistent the agent gets. Three reasons:
- Rules conflict with each other at length. The agent satisfies whichever it noticed last.
- Rules conflict with the code more often, because there are more rules and the code hasn’t kept up.
- Rules get truncated when the context window fills. The first 500 lines and the last 500 lines are what the agent sees; the middle is lost.
The trick is not more rules. The trick is fewer answers to each question so most rules become unnecessary.
The principle: one canonical answer per question
A codebase is ready for AI agents when:
For every question an agent might ask while writing a change, there is exactly one correct answer, and that answer is findable by reading either a 50-line rule file or a single grep.
Examples of “questions”:
- “Where do I put a new component?”
- “What’s the right way to make a form field?”
- “What colour is the primary CTA?”
- “Where does pagination state live?”
- “What’s the import path for
Drawer?” - “What’s the error-handling pattern for mutations?”
In an agent-ready codebase, each of those has one answer. In a typical legacy codebase, each has three to five.
The work of “preparing for agentic coding” is deleting answers until one remains.
What agent-ready does not mean
Some misconceptions to clear up:
It doesn’t mean rewriting everything. Most of your codebase is fine. The issue is the seams — the parts where old and new patterns coexist without a stated direction.
It doesn’t mean strict TypeScript everywhere immediately. That’s a target, not a starting point. The starting point is stopping the drift — making sure new code is correct even if old code isn’t yet.
It doesn’t mean banning the old patterns. It means explicitly marking them as frozen, with a stated path to deletion or migration.
It doesn’t mean a perfect design system. It means tokens that are internally consistent — names that match values, semantics that match namespaces, one canonical place per concept.
Coming next
In Part 2 I’ll walk through a concrete 3-week plan to take a legacy React codebase from “agent confuses” to “agent helps.” Week 1 is the audit and the lint gate (the cheap stuff that stops the bleeding). Weeks 2 and 3 are the architectural collapse (the expensive stuff that unlocks the wins).
In Part 3 I’ll dig into the rules themselves: how to write CLAUDE.md / AGENTS.md / .cursor/rules/ so that the rules are enforced, not aspirational — and how to use pre-commit hooks and CI to make the rule the path of least resistance.
The short version: agents are great at writing code in well-shaped codebases. Most of the work is shaping the codebase, not configuring the agent. Once you do, the agent stops being a coin flip and starts feeling like a senior engineer who actually read the docs.
Agent-ready React
6 parts in this series.
A six-part series on making legacy React codebases ready for AI coding agents.
- 01 Why Your Legacy React Codebase Confuses AI Coding Agents ← you are here
- 02 A 3-Week Plan to Make Your Legacy React Codebase Agent-Ready up next
- 03 Rules That Agents Actually Follow: Enforcement Over Aspiration
- 04 What to Put in design.md: A Complete Template
- 05 Writing Task-Specific Agent Prompts That Work First Try
- 06 Session-Start Hooks That Pay for Themselves
What did you take away?
Thoughts, pushback, or a story of your own? Drop a reply below — I read every one.
Comments are powered by Disqus. By posting you agree to their terms.