← Back to Blog
For: AI Engineers, ML Engineers, Platform Engineers, AI Systems Architects

Claude Code on Brownfield Codebases: The Harness Is the Product

On a new project the agent shapes your conventions; on a legacy one it must obey them - and that inversion is where most teams get burned.

#claude-code#coding-agents#context-engineering#brownfield#developer-tools

The pull request looked fine: a new process_refund function that Claude Code had written against a legacy payments codebase, a test, a clean two-file diff. It passed review at a glance, and it merged. Then the on-call engineer noticed that refunds were not showing up in the observability dashboard, and that a failed refund returned a generic 500 instead of the structured error the payments gateway expected.

The code was not buggy in the usual sense. It was architecturally wrong. It called logging.getLogger(__name__) instead of the house get_logger() that injects the trace and tenant IDs every other module uses. It queried Payment.objects.get(id=...) directly instead of going through PaymentRepository, bypassing the read-replica routing and the soft-delete filter that every other access path respects. It raised a bare ValueError where the rest of the service raises DomainError(code=...) so the API layer can map it to the right HTTP status. Three decisions, all defensible in isolation, all wrong for this codebase.

The agent did not lack intelligence. It lacked the one thing a new engineer gets on day one and an agent gets never: someone pointing at the existing code and saying "do it like that."

The thesis: on brownfield, the harness is the bottleneck, not the model

Here is the claim this article owns. On an existing codebase, the raw capability of the model is not what limits the quality of what a coding agent produces. The harness is - the context you engineer around the model: what it reads, in what order, under what instructions, with what tools. Teams keep reaching for a smarter model to fix architecturally-wrong output. A smarter model does help at the margin - a larger window, better recall over the context it does hold - but it cannot recover conventions that never entered its context in the first place, so it produces more fluent architecturally-wrong output. The failure is structural, not intelligence-limited.

I want to name the mechanism precisely, because "the model needs more context" is too vague to act on. I call it Convention Inversion.

On a greenfield project, the agent is the convention-author. It picks the logger, the error type, the folder layout, and because it picked them, its later choices are consistent with its earlier ones by construction. Its guesses match because it is guessing against itself.

On a brownfield codebase, that role inverts. The agent becomes a convention-follower. The logger, the error type, the access pattern, and a thousand other decisions were made years ago, by people who have since left, and are recorded nowhere the agent can read them. Every naive-default failure - wrong logger, wrong query path, wrong error type - traces back to this single inversion. The agent is still authoring conventions, confidently, in a codebase that already has them.

One clarification before we go further, because "Convention Inversion" will make some readers reach for the terms next to it. This is not Inversion of Control, where a framework calls your code instead of the reverse. It is not Convention over Configuration, where sensible defaults replace explicit wiring. And it is not "context debt," the useful term others have coined for the accumulated undocumented decisions in a legacy system. Convention Inversion names something narrower and more actionable: the moment the author of a set of conventions and the party bound to follow them come apart. On greenfield they are the same party - the agent authors its own conventions and obeys them by construction. On brownfield they split - the agent must follow conventions authored by others it cannot see. Everything in the harness exists to close that gap. Hold onto the general form - author and follower coming apart - because it returns at a second altitude near the end, where the author turns out to be you.

The uncomfortable part, if you were expecting a vendor to disagree: Anthropic already says this. The Claude Code best-practices documentation opens by stating that "most best practices are based on one constraint: Claude's context window fills up fast, and performance degrades as it fills." The "just wait for a smarter model" position is not Anthropic's engineering guidance - it lives in the market, in the demos, in the procurement decks. The people building the tool are telling you the constraint is the harness. Most teams are not listening.

Why Convention Inversion gets worse in production, not just in a demo

The demo works because the demo is greenfield. Someone opens an empty repo, describes an app, and the agent builds something coherent in an afternoon. That is a real capability and it is genuinely impressive. It is also the easy case, because in an empty repo the agent's guesses cannot collide with anything.

Production is the other case. Your codebase is measured in millions of lines; a context window is 200K tokens on the standard configuration, and up to 1M where an extended window is available. Even at a million tokens you are holding a small fraction of the system at any moment, and - critically - the model does not get more reliable as you fill that window. Independent testing by Chroma across 18 frontier models found that every one of them, including the strongest, becomes less reliable at recalling and using information as the input grows, and the degradation shows up well before the window's stated limit. Anthropic frames the same effect as a limited "attention budget." More context is not more understanding past a point; it is more noise the model has to hold in working memory - which is why the window is infrastructure to manage, not a bucket to fill.

Now scale that across a team. One developer running an unguided session and producing one architecturally-wrong PR is a code-review problem. Ten developers running unguided sessions, each agent re-inventing the house conventions slightly differently, is an entropy problem. The real enterprise risk of coding agents on legacy code is not the single bad pull request that review catches. It is that the codebase drifts faster than it did before - toward ten subtly different logging patterns, three competing ways to fetch a payment, a slow erosion of the consistency that made the system legible in the first place. Convention Inversion is not a per-session bug. Without a shared harness, it is a per-developer, per-session tax on your architecture. Staying valuable as the engineer in that loop, rather than the person who pastes what the agent produced without understanding it, is the human-side mirror of the same problem.

Why agentic search changes where the setup effort goes

To fix the harness you have to understand how the agent actually finds things, because it is not what most people assume.

Claude Code does not maintain a vector index of your repository. It navigates the way an engineer does: it runs grep, it reads files, it follows a reference from one file to the next, loading context just in time rather than from a pre-built index. This is a deliberate design choice, and Anthropic has been explicit about it. Boris Cherny, who created Claude Code, has said that early versions used a retrieval-augmented generation (RAG) setup with a local vector database, and that they moved away from it because agentic search worked better in their testing. Another engineer on the team put it more bluntly: agentic search outperformed the indexed approach "by a lot," and the margin surprised them.

That result is counterintuitive if you have internalized "an index means better retrieval," so it is worth understanding why it holds. An index is a snapshot; it goes stale the moment the agent edits a file, and it returns fuzzy nearest-neighbor matches that are often almost-right in a way that is worse than a miss. Agentic search reads the current state of the code and reasons about what it finds. It is fresh, precise, and private - nothing has to be embedded and stored.

But the property that makes agentic search work is also the property that makes brownfield hard, and this is the mechanism that earns the rest of this article: with no index, navigation quality is bounded entirely by how legible you have made the codebase. An index can paper over bad naming with vector similarity. Agentic search cannot. If your payment logic lives in a file called utils2.py next to helpers_old.py, and the function is named do_the_thing, the agent has nothing to follow. It greps for "refund," lands on a comment in an unrelated module, and starts building from the wrong place. On a well-named, consistently-structured codebase the same agent walks straight to the right file. The model is identical in both cases. The harness - here, the legibility of the code and the signposts you have placed in it - is the entire difference.

The wrong way: one unbounded Claude Code session that does everything

The default way people use a coding agent on a legacy codebase is a single prompt in a single session:

code
Investigate the codebase and add refund support to the payments service.

Watch what happens. The agent, correctly trying to be thorough, starts investigating. It greps for "payment." It gets forty hits. It reads the models, then the serializers, then the service layer, then the tasks, then the tests, then the three utility modules that came up in the grep, then the API views that import them. It is now two hundred files deep. Anthropic has a name for this failure - "the infinite exploration" - where the agent reads hundreds of files and fills its own context before it has written a line.

By the time it starts implementing, the window is saturated. The early part of the conversation - your actual instruction, the first useful file it found - has decayed under everything that came after. This is context rot in practice: the signal that mattered is now buried in the noise the agent generated while looking for it. The refund code it finally writes is drafted from a degraded, half-remembered picture of the system. It writes logging.getLogger. It writes Payment.objects.get. The one session tried to do discovery and construction at once, and the discovery poisoned the construction.

The tempting fix is the wrong one. You add "use the existing logger and repository pattern" to the prompt. Now you are hand-feeding conventions one incident at a time, forever, and the next developer's session knows none of it. You have treated a symptom of Convention Inversion, in one session, by hand. The disease is untouched.

The right way: split exploration from editing

The fix is to stop asking one session to both understand and change the system. Split it in two: a read-only exploration pass that produces a durable map, and a clean editing pass that works from that map. This is not a fringe technique - it is Anthropic's recommended default working mode, documented as Explore, then Plan, then Implement, then Commit, with the note that "letting Claude jump straight to coding can produce code that solves the wrong problem."

The mechanism that makes it cheap is the subagent. A subagent runs in its own separate context window and reports back a summary, not the raw files it read. Anthropic's own framing: "the subagent explores the codebase, reads relevant files, and reports back with findings, all without cluttering your main conversation." The two hundred files get read in a context you throw away. What survives is the map.

First, the exploration pass. Define a read-only explorer so it physically cannot edit while it investigates:

code
# .claude/agents/payments-explorer.md---name: payments-explorerdescription: Read-only mapper for the payments subsystem. Produces a findings  file; never edits code.tools: Read, Grep, Glob---You are mapping an existing subsystem so a later editing session can workwithout re-reading the whole tree. You do not write or modify code.Produce a findings file with exactly these sections:1. Entry points - where a request enters this subsystem2. The house patterns - logger, data access, error handling, with ONE   file:line example of each that the editing session should copy3. The files a refund feature would touch, and why4. Gotchas - anything surprising that a naive change would get wrong

Run it, pointed at the task, and have it write the map to a file in the repo:

code
Use the payments-explorer subagent to map how refunds would be added to thepayments service. Write the result to notes/payments-refund-map.md.

The subagent burns its own context reading the forty files. Your main session stays clean. The output is a small, high-signal document:

code
# notes/payments-refund-map.md## Entry points (existing - what's here now)- API: PaymentView at payments/api/views.py:88 (DRF). RefundView goes beside it.- Async: charges settle in payments/tasks/charges.py:20. No refund task exists yet.## House patterns - copy these exactly- Logger:   payments/service/charge.py:14            `log = get_logger(__name__)` from app.observability            (NOT logging.getLogger - that skips trace/tenant IDs)- Data:     payments/repo/payment_repository.py:40            `PaymentRepository.find_by_id()` - routes reads to the replica            and applies the soft-delete filter. Never use Payment.objects.- Errors:   payments/errors.py:12  raise DomainError(code="refund_failed")            The API layer maps DomainError.code -> HTTP status. Bare            exceptions become a 500.## Files a refund touches- payments/service/refund.py (new)  - mirror payments/service/charge.py- payments/repo/payment_repository.py - add refund persistence method- payments/api/views.py - add RefundView next to PaymentView- payments/tasks/refunds.py (new) - async settlement, mirror charges.py## Gotchas- Charges and refunds share the ledger; a refund must post a REVERSAL  entry, not a negative charge. See ledger/README.md.

Now the editing pass, in a fresh session with the window empty. This is the other half of Anthropic's documented pattern - once the specification exists, "start a fresh session to execute it" so the new session "has clean context focused entirely on implementation." You give it the map and point at the one example to copy:

code
Read notes/payments-refund-map.md. Implement refund support following itexactly. Match the logger, data-access, and error patterns from thefile:line examples in the map. Model payments/service/refund.py on payments/service/charge.py.Do not read the rest of the codebase unless the map is missing something.

The editing session never does open-ended discovery. It loads a curated map and one reference file, and it writes code that matches the house patterns because you handed it the house patterns instead of hoping it would find them. Convention Inversion is defeated the same way you would onboard a human: not with a smarter engineer, but by pointing at the existing code and saying "like that."

Convention Inversion and the layers of the harness

The exploration/editing split is the working mode. Underneath it sits the durable part of the harness - the layered instructions and tools that make every session start further ahead. Here is the whole stack, in the order you should invest in it.

BROWNFIELD HARNESS ENGINEERING · CLAUDE CODEThe Brownfield Harness StackSix layers, in the order to invest when onboarding an agent to a legacy codebase1 · Instruction HierarchyRoot: pointers + gotchas onlyNested files load as agent walks tree2 · Commands + ExclusionsPer-dir test/lint commandsExclude generated + vendored files3 · Codebase MapWhen dirs don't carry the loadWhere things live, and why4 · Symbol NavigationGo-to-def / find-refs over grepPlugin or Serena MCP, 30+ langs5 · Explore / Edit SplitRead-only pass writes findings fileFresh session edits from the map6 · MCP / IntegrationsWire these LASTOnly after the basics workTHE ORDERING RULEInstructions first. Integrations last.Most teams wire MCP servers and integrations first,then wonder why output ignores house conventions.The context basics - hierarchy, commands, symbols -are what make every later layer pay off.Get 1-3 right before you touch 6.CONVENTION INVERSIONThe harness is the productGreenfield: the agent authors your conventions.Brownfield: it must follow decisions already bakedinto hundreds of prior choices it cannot see.A smarter model does not fix a missing harness.The bottleneck is structural, not intelligence.Engineer the harness, not the prompt.

Layer 1: The CLAUDE.md instruction hierarchy

The root CLAUDE.md is loaded into every session, so it is the most expensive real estate you own. The discipline is to keep it to pointers and gotchas, not documentation. Anthropic's guidance is explicit and worth internalizing: "Bloated CLAUDE.md files cause Claude to ignore your actual instructions." For each line, ask whether removing it would cause a mistake; if not, cut it. What belongs there: the non-obvious build command, the one architectural decision that trips everyone up, a pointer to where the real conventions live. What does not: anything the agent can infer by reading the code, and file-by-file descriptions that go stale in a week.

The leverage comes from nesting. Claude Code loads CLAUDE.md files from parent directories at startup, and pulls in a child directory's CLAUDE.md on demand when it reads a file in that directory. So the payments module's local conventions live in payments/CLAUDE.md and cost nothing until the agent actually works in payments:

code
# payments/CLAUDE.md  (loads only when the agent touches this directory)- Logger: `get_logger(__name__)` from app.observability. Never logging.getLogger.- Data access: go through PaymentRepository. Never Payment.objects directly.- Errors: raise DomainError(code=...). Bare exceptions become HTTP 500.- Refunds post a ledger REVERSAL, not a negative charge. See ledger/README.md.

That is the antidote to Convention Inversion made durable. The conventions load additively as the agent walks the tree, so it reads exactly the rules for the code it is touching and pays no token cost for the rest.

Layer 2: Scoped commands and exclusions

Put the local test and lint command in the subdirectory's CLAUDE.md so the agent runs pnpm --filter @repo/api test instead of the whole monorepo suite. For exclusions, one caution that matters: an ignore file keeps generated and vendored files out of the agent's way, but do not treat it as a security boundary. Claude Code has been reported reading .env contents despite a .claudeignore entry meant to block them (The Register reproduced the issue in January 2026). If a path must never be read, enforce it with permissions.deny in .claude/settings.json or a PreToolUse hook - a real gate, not a hint, and the same deterministic-constraint thinking that keeps agent tool calls in scope:

code
// .claude/settings.json{  "permissions": {    "deny": ["Read(./.env)", "Read(./secrets/**)"]  }}

Layer 3: A codebase map

When the directory structure does not carry the load - a legacy tree where names lie about contents - a short map document is worth more than any amount of grep. It says where things live and, more importantly, why. This is also exactly the artifact a coding agent can generate for you as a one-time investment; I have written separately about going from an unknown codebase to an architecture document, and that output is the seed of your map. Keep it alive the way you would keep a system diagram current rather than letting it rot into a stale screenshot in someone's Drive.

Layer 4: Symbol-level navigation over grep

This is the highest-value move for a legacy multi-language repo, and it is underused. String matching lands on the wrong symbol constantly: grep for send in a large codebase and you get the mailer, the message queue, the metrics emitter, and a comment. Symbol resolution - go-to-definition, find-references - filters to the actual definition before the agent reads anything, using the same Language Server Protocol (LSP) your IDE uses. This is now available first-party: Anthropic recommends installing a code-intelligence plugin for typed languages to give the agent "precise symbol navigation and automatic error detection after edits." It is also available through Model Context Protocol (MCP) servers such as the open-source Serena, which exposes find_symbol and find_referencing_symbols backed by real language servers across 30+ languages. Either way, symbol navigation turns "read ten files to find the one that defines this" into a single precise jump.

Layer 5: The exploration/editing split

This is the working mode from the previous section, promoted to a first-class layer because it is the habit that makes every other layer pay off. The instruction hierarchy, the scoped commands, the symbol navigation - none of them help if the session using them is already drowning in its own discovery. A read-only exploration subagent writes the findings file; a fresh session edits from it. Make it the default, not the exception.

Layer 6: MCP and integrations, wired last

MCP servers and external integrations - issue trackers, databases, design tools - come last, for the reason the checklist makes concrete. They multiply the reach of a working harness, and they multiply the confusion of a broken one. Wired before layers 1 through 5 exist, they give a context-starved agent more ways to act on a codebase it still does not understand.

The two-lane flow: context saturation versus a scoped edit

The whole argument, in one picture: the same task, run two ways.

mermaid
flowchart TD
    T[Task: add refund support<br/>to the payments service]

    T --> A1[Single unbounded session]
    A1 --> A2["investigate the codebase<br/>and add the feature"]
    A2 --> A3[Reads 200+ files<br/>grep wanders, follows every reference]
    A3 --> A4[Context window saturates<br/>early instructions decay]
    A4 --> A5[Architecturally-wrong PR<br/>wrong logger, wrong ORM, bare errors]

    T --> B1[Exploration / editing split]
    B1 --> B2[Read-only exploration subagent<br/>maps the subsystem in throwaway context]
    B2 --> B3[Writes findings file<br/>payments-refund-map.md]
    B3 --> B4[Fresh editing session<br/>loads only the map + one example file]
    B4 --> B5[Scoped, convention-correct PR<br/>matches house patterns]

    classDef task fill:#FFD93D,color:#2C2C2A,stroke:#D4B02A;
    classDef neutral fill:#4A90E2,color:#FFFFFF,stroke:#3A7BC8;
    classDef split fill:#7B68EE,color:#FFFFFF,stroke:#6858DE;
    classDef fail fill:#E74C3C,color:#FFFFFF,stroke:#B03A2E;
    classDef good fill:#6BCF7F,color:#2C2C2A,stroke:#4CAF64;

    class T task;
    class A1,A2,A3,A4 neutral;
    class A5 fail;
    class B1 split;
    class B2,B3,B4 neutral;
    class B5 good;

Same model, same task, same codebase. The only variable is the harness.

The operational reality most guides skip: Convention Inversion, one level up

Here is the part that is counterintuitive enough that almost no one writes it down, and it is where I am giving you my reasoned position rather than a citable fact - I will mark exactly where the evidence ends.

Convention Inversion does not stop at the codebase. It recurs, one level up, in the harness itself. The instructions you write are conventions too, and the moment you commit them, you become the convention-author whose choices a future agent must follow - including a future, stronger agent for whom your choices are wrong.

Consider a rule that a lot of teams add in 2026: "Always refactor one file at a time. Never modify more than one file in a single change." For a weaker model that loses the thread across a multi-file edit, this is a sensible guardrail - it trades capability for reliability. For a stronger model that can hold a coherent five-file refactor in working memory and execute it correctly, the exact same rule is a handcuff. It forces an artificial, worse decomposition and produces more churn, not less. The instruction that helped your harness a year ago actively degrades it now. Your own convention has inverted on you.

The rule did not change; the model outgrew it. That is the second face of Convention Inversion, and it is the one nobody plans for. The first face is the agent failing to follow your codebase's conventions. The second is your harness conventions no longer deserving to be followed - written for a weakness the model has since shed, still silently steering it. Same inversion, one altitude up: the party that authored the conventions and the party that must live under them have traded places again, except this time the author was you.

This is where I have to be honest about what is proven. The direct evidence is thin: this is a reasoned position, not a benchmarked result. What supports it - short of proving it - is real. An empirical study of AI coding agents (arXiv:2511.04824) found that agents refactor with an inverse motivation profile compared to humans, rarely restructuring for modularity or deduplication the way people do; the shape of an agent's behavior is not fixed and shifts in non-obvious ways between systems. And Anthropic's own warning that a bloated instruction file causes the model to ignore your instructions tells you that harness configuration has real, sometimes negative, weight on behavior. Neither proves that a rule tuned for a weak model handcuffs a strong one. Together they make it the way to bet.

The practical consequence is a discipline, not a fact: harness configuration is not write-once. Every rule in your CLAUDE.md was written against a specific model's failure modes, and those failure modes move under you with every release. Budget a review cadence for your harness the way you budget one for dependencies. After a major model release, re-audit your instruction files and ask of each rule: is this still protecting me, or is it now protecting me from a weakness the model no longer has? This is the sense in which the harness is the product - not a setup you do once, but a living layer you maintain. It ties back to the thesis directly: if the bottleneck were the model, you would upgrade and move on. Because the bottleneck is the harness, upgrading the model is the moment your harness needs the most attention, not the least. This is the same reason autonomous loops need explicit stopping conditions rather than blind iteration, a point I develop in the Ralph loop and /goal.

The brownfield onboarding checklist

Onboard a coding agent to a legacy codebase in this order. The ordering is the advice; the most common mistake is doing it backward.

  1. Instruction hierarchy first. A lean root CLAUDE.md (pointers and gotchas, nothing the agent can infer) plus nested CLAUDE.md files that carry each module's local conventions - logger, data access, error handling - with a file:line example of each. This alone defeats most of Convention Inversion.
  2. Scoped commands and exclusions. Per-directory test and lint commands so the agent runs the right suite. Exclude generated and vendored files. Enforce genuine secrets exclusion with permissions.deny or a hook, not an ignore file.
  3. Symbol navigation. Install a code-intelligence plugin for your typed languages, or wire up an LSP-backed MCP server. On a multi-language legacy repo this is the single highest-value tool you can add.
  4. Make the exploration/editing split your default. Read-only exploration subagent writes a findings file; a fresh session edits from it. Stop running discovery and construction in one window.
  5. MCP and integrations last. This is the "don't do this first" rule. Wiring your issue tracker, your database, and five MCP servers before the instruction hierarchy exists is the most common way teams waste the first month. Integrations multiply what a working harness can do and multiply the confusion of a broken one. Get layers 1 through 4 right, then add reach.
  6. Schedule a harness re-audit. Put a recurring reminder to review your instruction files after every major model release. Delete the rules that were protecting you from a weaker model's mistakes.

If you take one thing: on brownfield code, stop evaluating models and start engineering the harness. The model you already have is almost certainly not your bottleneck. This is the coding-agent instance of a broader pattern I have argued across the Harness Engineering series - there the harness wraps a production LLM service; here it wraps a coding agent, but the discipline is the same. The intelligence is in the model. The leverage is in the harness.

References


Agentic AI

Follow for more technical deep dives on AI/ML systems, production engineering, and building real-world applications:


Comments