Updated 11 November 2026. This article first ran on 17 July 2026. The argument stands, but four things in it had drifted from Claude Code's current documentation. Anthropic rewrote the best-practices guide this article quoted, so every quotation now matches the page as it reads today. The original presented .claudeignore as a way to keep generated files out of the agent's reach, and that file is not a supported Claude Code control; Layer 2 now uses .gitignore and Read deny rules. The 200K context-window figure is gone, because most current models have a 1M-token window. And the case for subagents now rests on what they still buy you, not on protecting a window that rarely runs out.
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 turning to a smarter model to fix architecturally-wrong output. A smarter model does help at the margin, through a larger window and better recall over the context it holds. But it cannot recover conventions that never entered its context in the first place. So it produces output that is more fluent and still architecturally wrong. The failure is structural, not a limit of intelligence.
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.
Put the two side by side and the difference comes down to one edge:
classes: {
purple: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
mint: {style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
}
grid-rows: 1
grid-gap: 48
greenfield: "Greenfield" {
label.near: top-center
agent: "Agent" {class: purple}
conv: "Conventions it wrote\n(logger, errors, layout)" {class: mint}
agent -> conv: "authors"
conv -> agent: "follows"
}
brownfield: "Brownfield" {
label.near: top-center
people: "Engineers who\nhave since left" {class: grey}
conv: "House conventions,\nwritten down nowhere" {class: mint}
agent: "Agent" {class: purple}
people -> conv: "authored"
conv -> agent: "must follow,\ncannot see" {style.stroke-dash: 4}
}
On the left the agent's guesses match because it is the one that made them. On the right the dashed edge is the whole problem, and every layer of the harness later in this article exists to make it solid.
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. Keep the general form in mind - author and follower coming apart - because it returns near the end, one level up, where the author turns out to be you.
The uncomfortable part, if you were expecting a vendor to disagree: Anthropic already says this. Its guide to running Claude Code in large codebases warns that as a codebase grows, "the defaults tuned for smaller projects can fill the context window with instructions and file reads unrelated to the task, costing tokens and degrading Claude's performance." The remedy it gives is not a different model. It is scoped instruction files, blocked reads of generated and vendored code, and language-server lookups instead of file scans, which is most of the harness this article builds. 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. Anthropic's best-practices guide now says that "most current models have a 1M-token window and compaction happens on its own, so running out is rare." Running out was never the real problem. Even at a million tokens, you hold only a small fraction of the system at any moment. And 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 does not mean more understanding. Past a point, it is just more noise the model has to hold in working memory. This 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.
What separates the two outcomes is where the conventions live:
classes: {
blue: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
purple: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
green: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
red: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
}
grid-rows: 1
grid-gap: 48
none: "No shared harness" {
label.near: top-center
a: "Session A" {class: blue}
b: "Session B" {class: blue}
c: "Session C" {class: blue}
pa: "logging.getLogger()" {class: grey}
pb: "structlog, set up inline" {class: grey}
pc: "get_logger()" {class: grey}
drift: "Three logging patterns\nin one service" {class: red}
a -> pa
b -> pb
c -> pc
pa -> drift
pb -> drift
pc -> drift
}
shared: "Shared harness" {
label.near: top-center
file: "payments/CLAUDE.md\n(committed)" {class: purple}
a: "Session A" {class: blue}
b: "Session B" {class: blue}
c: "Session C" {class: blue}
one: "get_logger() in every PR" {class: green}
file -> a
file -> b
file -> c
a -> one
b -> one
c -> one
}
On the left each session invents an answer, and review sees three pull requests that each look reasonable on their own. On the right the answer is a file in the repository, so every session starts from the same one.
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. One artifact later in this article looks like an exception to this. The codebase map in Layer 3 is a small, static snapshot, the very thing I am warning against here. It is a deliberate exception, and small enough that it does not bring the staleness back. I close that loop when I reach Layer 3.
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 hide bad naming behind 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.
Here is the same search against both trees:
classes: {
blue: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
mint: {style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
salmon: {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
green: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
red: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
}
grid-rows: 1
grid-gap: 48
legible: "Legible tree" {
label.near: top-center
q: "grep \"refund\"" {class: blue}
hit: "payments/service/refund.py" {class: mint}
out: "Starts from the right file" {class: green}
q -> hit -> out
}
illegible: "Illegible tree" {
label.near: top-center
q: "grep \"refund\"" {class: blue}
hit: "a comment in\nnotify/mailer.py" {class: grey}
miss: "utils2.py: do_the_thing()\n(the real logic, never matched)" {class: salmon}
out: "Builds from the wrong place" {class: red}
q -> hit -> out
q -> miss: "no match" {style.stroke-dash: 4}
}
The query and the model are the same in both panels. What differs is whether the names in the tree let a text search reach the code that matters.
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:
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, and it has not written a line.
Laid out in the order it entered the window, the session looks like this:
classes: {
yellow: {style: {fill: "#FFD93D"; stroke: "#D4B02A"; font-color: "#2C2C2A"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
red: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
}
window: "One session's context, in the order it filled" {
label.near: top-center
grid-rows: 6
grid-columns: 1
grid-gap: 6
i: "1. Your instruction, and the first useful file" {class: yellow}
g: "2. Forty grep hits for \"payment\"" {class: grey}
m: "3. Models, serializers, the service layer" {class: grey}
t: "4. Tasks, tests, three utility modules" {class: grey}
v: "5. The API views that import them: 200 files in" {class: grey}
w: "6. Refund code drafted here: getLogger, Payment.objects" {class: red}
}
By the time it starts implementing, at row 6, the window is full of what it read on the way. On a current model there is still room left, and that is the trap: nothing stops the session, so nothing tells you it has gone wrong. The early part of the conversation - your actual instruction, the first useful file it found - has decayed under everything that came after, and if the session compacts, what remains of it is a summary. 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. Anthropic's best-practices guide describes the same shape: write the plan to a file first, then build from that file in a fresh session.
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. Claude Code already does a version of this on its own: broad searches go to a built-in Explore agent, which, in Anthropic's words, "keeps exploration results out of your main conversation context." The two hundred files get read in a context you throw away. What survives is the map.
Follow the numbered edges and watch what crosses from one context to the other:
classes: {
blue: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
purple: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
yellow: {style: {fill: "#FFD93D"; stroke: "#D4B02A"; font-color: "#2C2C2A"}}
green: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
}
direction: down
explore: "Discarded context" {
label.near: top-center
ex: "payments-explorer\ntools: Read, Grep, Glob" {class: purple}
files: "40 files read,\n3 dead ends" {class: grey}
ex -> files: "1. reads"
}
map: "notes/payments-refund-map.md" {class: yellow}
s: "Fresh editing session" {class: blue}
ref: "payments/service/charge.py\n(one example to copy)" {class: grey}
pr: "Convention-correct PR" {class: green}
explore.ex -> map: "2. writes"
map -> s: "3. loads"
ref -> s: "4. copies the pattern"
s -> pr: "5. writes"
Only the findings file crosses between the two contexts. Everything the explorer read stays on its side and is discarded with it.
Be clear about what you are paying for, because it is not a smarter answer. Anthropic's guide is direct that a subagent costs mostly tokens, can end up slower than doing the task in place, and by default runs the same model or a smaller one. It no longer sells subagents as a way to protect the context window. On brownfield code the custom explorer below buys you three things the built-in Explore agent does not give you by itself. It writes a findings file that a fresh session can start from. Its fixed schema forces the house patterns out as file:line examples. And its tool list cannot edit. There is a fourth difference that matters here more than anywhere: the built-in Explore and Plan agents skip your CLAUDE.md files, so they never see the conventions you wrote down. A custom subagent loads them the way the main session does.
First, the exploration pass. Define a read-only explorer so it physically cannot edit while it investigates:
# .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 wrongRun it, pointed at the task, and have it write the map to a file in the repo:
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:
# 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 the pattern. Anthropic's guide puts it for a spec: once the file exists, "start a fresh session to build from it, so the build begins with a clean context and a written definition of done." Here the findings file plays the part of the spec. You give it the map and point at the one example to copy:
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.
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: target under 200 lines per file, because "Longer files consume more context and reduce adherence." Its best-practices guide now lists describing your architecture and file layout in CLAUDE.md among the habits you can stop, with the reason "Claude reads the repo itself. Only write down what isn't in there." 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:
# 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.Two newer controls sharpen this layer. Path-scoped rules in .claude/rules/, each with a paths: glob in its frontmatter, load only when Claude works with matching files, so one data-access rule can cover every repository module without a CLAUDE.md in each directory. And the claudeMdExcludes setting skips files you never want loaded, such as other teams' packages or a legacy subtree whose instructions no longer match the code. One caveat applies to both nested files and path-scoped rules: they enter the conversation when their trigger file is read, so compaction summarizes them away with everything else. A rule that must hold for the whole session belongs in the root file. Run /context all to see which instruction files actually loaded.
For a session that edits refunds, the result looks like this:
classes: {
blue: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
green: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
salmon: {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
}
direction: down
root: "CLAUDE.md at the repo root\nloads at launch" {class: blue}
pay: "payments/CLAUDE.md\nloads when Claude reads payments/" {class: green}
rule: ".claude/rules/data-access.md\npaths: **/repo/*.py" {class: green}
api: "services/api/CLAUDE.md\nnot loaded: never touched" {class: grey}
legacy: "legacy/CLAUDE.md\nskipped by claudeMdExcludes" {class: salmon}
root -> pay: "on demand"
root -> rule: "on a matching file"
root -> api
root -> legacy: "excluded" {style.stroke-dash: 4}
Only the blue and green files cost tokens in this session. The grey one waits until someone works in the API package, and the excluded one never loads at all.
There is a second-order effect worth naming: the root file does not just cost tokens, it sets the ceiling on how closely the agent follows anything you write. When instructions compete for attention the marginal one does not get half-followed - it gets dropped, and you cannot predict which. A 400-line CLAUDE.md that documents every module reads to the model as low-signal noise, and the single line that actually matters (refunds post a ledger reversal, not a negative charge) drowns in it. That is the mechanism behind Anthropic's warning - bloat is not a tidiness problem, it is an instruction-adherence problem. Treat the root file's length as a budget you actively defend, and re-audit it the moment it grows past a screen. The nested files exist precisely so that this discipline costs you no coverage. My book Harness Engineering for Production AI Systems works the same problem from the application side, where you assemble the context yourself instead of letting an agent find it: Chapter 5, "Context Assembly as Managed Infrastructure," treats the window as a priority order rather than a size limit, and shows how to measure whether a constraint placed at the head, the middle, or the tail of a long context actually gets followed.
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 correction to a common assumption: .claudeignore is not a Claude Code feature. Anthropic does not document it, and The Register reproduced Claude Code reading .env contents that a .claudeignore entry was meant to block (January 2026). That was not a bug in the file's handling, because nothing reads the file. The controls that exist are .gitignore, which Claude's content searches already respect, and Read rules in permissions.deny. 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:
{ "permissions": { "deny": ["Read(./.env)", "Read(./secrets/**)"] }}Both halves of this layer are really about the length and cleanliness of the agent's feedback loop.
A scoped command shortens the loop. The unit of work for a coding agent is edit → verify → edit, and it pays the verify cost after every change, so the duration of that step sets the tempo of the whole session. Point the agent at the full monorepo suite and a one-line edit costs a two-minute, four-thousand-test run; point it at the package it actually touched and the same check comes back in seconds:
# services/api/CLAUDE.md (loads only when the agent works in services/api)- Test this package: pnpm --filter @repo/api test- Lint + typecheck: pnpm --filter @repo/api lint && tsc -p services/api --noEmit- Do NOT run root `pnpm test` - 4k tests across 30 packages, minutes per run.Speed is not the only payoff; the other is attribution. A red result in the API package's own forty tests points straight at the edit that caused it. The same failure buried in four thousand monorepo tests tells the agent only that something broke somewhere, and it will burn context re-running and bisecting to find out where. And there is a behavioral failure mode waiting past the mechanical one: an agent that has to wait minutes for feedback quietly learns to stop waiting - it assumes the change was fine and moves on, which is exactly the unverified edit the loop existed to catch. A fast, scoped check is what keeps verify-after-every-edit a habit the agent actually keeps. Keep that scoping in the local loop, though: why monorepos help coding agents with proof, not context comes down to CI running every affected consumer's checks before merge.
Exclusions keep the loop clean. Agentic search greps the working tree, and a legacy tree is full of files that match every search term while teaching the agent nothing: build output (dist/, .next/), vendored dependencies, generated clients (protobuf stubs, OpenAPI and GraphQL codegen), lockfiles, and test snapshots. Grep for refund in that tree and the top hit is a minified bundle with the entire app on one 60,000-character line, followed by a vendored copy of a library and a generated API client - three confident matches, none of them the code you want. That costs you twice. The real hit in payments/service/refund.py is buried under the noise, and a single match inside a minified file dumps tens of thousands of tokens of one "line" into the agent's window. Two controls keep that class of file out. Build output and installed dependencies are usually in .gitignore already, and Claude's content searches skip them by default. For noise that is committed - a vendored SDK, generated clients, test snapshots - add Read deny rules to the same deny list in .claude/settings.json, which stop Claude opening those files:
{ "permissions": { "deny": [ "Read(./**/__generated__/**/*)", "Read(./**/vendor/**/*)", "Read(./**/*.min.js)", "Read(./**/*.snap)" ] }}The directory patterns end in /**/* rather than /**, so Claude can still list the directory but cannot open what is inside it. Know where the rules stop. They cover Claude's own file tools, and the shell commands it recognizes, such as cat and grep, when a denied path is an argument. A grep -r over a parent directory still prints matches from denied files. Relative patterns anchor at the directory you start Claude from, not at the repository root. For a hard limit on what a shell command can touch, /sandbox confines it at the operating-system level and picks up the same deny rules.
Here are the four controls people reach for, and where each one stops:
classes: {
head: {style: {fill: "#E8E8E8"; stroke: "#CCCCCC"; font-color: "#2C2C2A"; bold: true}}
ctl: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
cell: {style: {fill: "#FFFFFF"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
bad: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
badcell: {style: {fill: "#FFA07A"; stroke: "#D9785A"; font-color: "#2C2C2A"}}
}
grid-rows: 5
grid-columns: 3
grid-gap: 8
h1: "Control" {class: head}
h2: "Keeps out" {class: head}
h3: "Does not keep out" {class: head}
g1: ".gitignore" {class: ctl}
g2: "Ignored paths, from\nClaude's content searches" {class: cell}
g3: "Committed generated or vendored\ncode; a direct read of the file" {class: cell}
d1: "Read deny rules" {class: ctl}
d2: "Claude's file tools; cat or grep\nwith a denied path as an argument" {class: cell}
d3: "grep -r over a parent\ndirectory; subprocess reads" {class: cell}
s1: "/sandbox" {class: ctl}
s2: "Shell commands, at the\noperating-system level" {class: cell}
s3: "Claude's built-in tools,\nwhich deny rules cover" {class: cell}
c1: ".claudeignore" {class: bad}
c2: "Nothing: Claude Code\ndoes not read the file" {class: badcell}
c3: "Everything" {class: badcell}
Each row has a gap in its right-hand column, which is why the controls above are layered rather than chosen between. The last row is nothing but gap.
Put the narrowest command that still proves the change where the agent will look for it, exclude the files that only add noise, and every layer above this one runs on a tighter, less polluted loop.
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.
A map earns its place only when it answers questions the tree cannot. Three pay off most: where a responsibility actually lives when the directory name lies (the "billing" logic that really sits in core/legacy/txn.py), which of two near-identical modules is the live one and which is the deprecated copy nobody deleted, and the load-bearing indirection - the dependency-injection wiring or dynamic dispatch that grep will never connect for you. Keep it to exactly that. A map that restates the folder layout is worse than none, because it goes stale silently and the agent trusts it anyway. Date it, keep it short, and regenerate it as a cheap, repeatable pass rather than a document you maintain by hand forever.
Here is what that looks like as an artifact - a short map carrying only the three things the tree cannot tell the agent, built on the same payments example:
# docs/codebase-map.md (regenerated, not hand-maintained · last refreshed 2026-07)## Where the responsibility actually lives (the name lies)- "billing" is a misnomer: all charge/refund money movement is in payments/, NOT billing/ (billing/ holds invoice PDFs only).- Ledger posting lives in core/legacy/txn.py, not ledger/ (ledger/ is the read API over it).## Live vs deprecated (two near-identical modules)- payments/repo/payment_repository.py <- LIVE, use this- payments/legacy_repo.py <- DEAD (pre-2024), do not read or copy## Load-bearing indirection grep won't connect- PaymentProcessor is DI-bound in app/wiring.py:60; the concrete impl is StripeProcessor. grep "PaymentProcessor()" finds nothing.- Refund tasks dispatch by string name via CELERY_ROUTES in config/celery.py, so caller and task never reference each other by symbol.Every line here is something the agent would otherwise burn ten file-reads and a chunk of its window to reconstruct - and the last two (the DI binding, the string-dispatched task) it would never reconstruct from grep at all, because there is no textual reference to follow. That is the whole test for a map line: if grep or a glance at the folder tree would surface it, cut it; if only a human who has been burned by this codebase knows it, keep it.
Sort the facts by where they belong and the line is easy to draw:
classes: {
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
yellow: {style: {fill: "#FFD93D"; stroke: "#D4B02A"; font-color: "#2C2C2A"}}
}
grid-rows: 1
grid-gap: 48
tree: "The tree carries" {
label.near: top-center
grid-rows: 3
grid-columns: 1
grid-gap: 10
a: "Folder layout" {class: grey}
b: "Imports and call sites" {class: grey}
c: "File and function names" {class: grey}
}
map: "The map carries" {
label.near: top-center
grid-rows: 3
grid-columns: 1
grid-gap: 10
a: "Where a responsibility really lives\n(ledger posting in core/legacy/txn.py)" {class: yellow}
b: "Which of two near-identical\nmodules is the live one" {class: yellow}
c: "DI wiring and string dispatch\nthat grep cannot follow" {class: yellow}
}
If this feels like it contradicts the case against indexing I made earlier, that is the right question to raise. A map is a small index, and an index is the thing agentic search was meant to free you from. The resolution is in what each one is allowed to hold. The tree carries everything that is structurally true, and it cannot lie about it, because the tree is the code. The map carries only the few things the tree cannot express or actively misleads about - the three cases above. That narrow scope is also what defuses the staleness that sinks a full index. A snapshot of the whole repository rots on every edit. A map of only the non-navigable facts has almost no surface to rot, which is why you date it and regenerate it instead of maintaining it by hand. The findings file from the exploration split is safer still - it is generated fresh for each task and thrown away after the edit, so it never has time to go stale. The tree cannot lie about structure. But structure sometimes lies about meaning, and closing that gap is the map's entire job.
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 ships code-intelligence plugins that connect Claude Code to a language server, so it sees the type errors its own edits introduce and finds definitions and references by symbol instead of by text search. Install the language server binary first, then the plugin, for example /plugin install pyright-lsp@claude-plugins-official for Python. The plugins run in terminal sessions; cloud sessions do not start them. 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 for over 40 languages. Either way, symbol navigation turns "read ten files to find the one that defines this" into a single precise jump.
The payoff compounds with codebase age. In a young repo the agent can afford to read broadly; in a legacy tree the cost of reading the wrong ten files is the whole context window, and symbol resolution is what stops it paying that cost on every task. The setup is a one-time install - a plugin for typed languages, or Serena wired as an MCP server - and the return is that "find where this is defined" and "find everything that calls this" stop being read-the-repo operations. One caveat keeps it honest: navigation is only as good as the language server underneath it, so a repo that does not typecheck cleanly, or a dynamically-typed corner with no server support, degrades back to grep. Know where those edges are so you are not surprised when the agent's jumps turn vague there. And before you install one at all, weigh the standing cost: on most repositories code intelligence is a net loss until the codebase is large enough to earn it.
The difference is easiest to see on a single call. Ask where find_by_id is defined and grep hands back every repository in the tree; ask the language server for the references to one specific method and it hands back only the real callers:
# grep "find_by_id" -> every repository in the tree, no way to tell which is yoursaccount/repo.py:33: def find_by_id(...)order/repo.py:51: def find_by_id(...)payments/repo/payment_repository.py:40: def find_by_id(...) <- which one?# find_referencing_symbols("PaymentRepository.find_by_id") -> only the real callerspayments/service/charge.py:41payments/service/refund.py:33 (the file you are about to write)Wiring that up is a one-time entry in your MCP config. The launch flags below are the ones Serena documents for Claude Code - --context=claude-code tunes its tool set to the client, --project-from-cwd points it at the repo you launched from. This .mcp.json runs the latest source with uvx, so the whole team gets it from the repository; if you have Serena installed, serena setup claude-code registers it for you instead:
// .mcp.json — LSP-backed symbol navigation across 40+ languages{ "mcpServers": { "serena": { "command": "uvx", "args": ["-p", "3.13", "--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--context=claude-code", "--project-from-cwd"] } }}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.
The mechanism that makes this work is the discard. The exploration pass is allowed to be wasteful - open twenty files, chase three dead ends, burn its context - because its only job is to produce the findings file, after which it is thrown away. The editing session then starts from that distilled map with a clean window, so its tokens go to the change instead of the search. Skip the split and you get the failure mode this article opened with: one session that explores and edits in the same window, saturates halfway through, and starts making architectural guesses exactly when it has the least room left to reason. The findings file is the seam that lets you throw away the expensive half and keep the cheap one.
The one judgment the split needs is when to invoke it. It is a habit, not a law, and paying the setup cost - an explorer subagent, a findings file, a fresh session - only earns out when discovery is the expensive part of the task:
- Split when the change is cross-cutting, the subsystem is unfamiliar, or open-ended discovery would saturate the window before you ever reach the edit.
- Skip the split when the change is a single file you can already name - point a normal session straight at it, because spinning up the explorer buys nothing over just opening the file.
Asked in order, the two tests look like this:
classes: {
q: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
skip: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
split: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
}
grid-rows: 2
grid-columns: 2
vertical-gap: 70
horizontal-gap: 40
q1: "1. Is it one file you can already name?" {class: q}
a1: "Yes: skip the split and point\na normal session at that file" {class: skip}
q2: "2. Cross-cutting, unfamiliar, or would\ndiscovery swamp the session?" {class: q}
a2: "Yes: split. The explorer writes the map,\na fresh session edits from it" {class: split}
q1 -> q2: "no"
The worked example earlier in this article - the read-only payments-explorer writing payments-refund-map.md for a fresh editing session - is the shape to reach for the moment a task fails that "one file I can name" test.
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.
Concretely this is the issue tracker, the database inspector, the design tool, the internal service the agent can now call instead of being told about - each one widening what a competent agent does without leaving the session. But an integration is a force multiplier, and it multiplies whatever it is attached to. Wire a database MCP server into a harness where layers 1 through 5 already exist and the agent inspects the real schema to write a correct migration; wire the same server into a context-starved agent with no conventions and no map, and you have handed it a faster way to act confidently on a system it has not understood. The ordering rule is not aesthetic. Integrations amplify the harness, so there has to be a harness worth amplifying first.
Same server, two harnesses:
classes: {
purple: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
mint: {style: {fill: "#98D8C8"; stroke: "#5FA898"; font-color: "#2C2C2A"}}
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
green: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
red: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
}
grid-rows: 1
grid-gap: 48
with: "Layers 1 to 5 in place" {
label.near: top-center
db: "payments-db MCP server" {class: purple}
h: "Conventions, map,\nscoped commands" {class: mint}
out: "Reads the real schema,\nwrites a migration that fits" {class: green}
db -> out
h -> out
}
without: "No harness yet" {
label.near: top-center
db: "payments-db MCP server" {class: purple}
h: "No conventions,\nno map" {class: grey}
out: "Acts faster on a system\nit has not understood" {class: red}
db -> out
h -> out
}
The server is identical in both panels. It multiplies whatever it is attached to, which is why it goes in last.
The database server is the sharpest case, and it carries a second discipline beyond ordering: wire it read-only. The config is a one-time entry, and the credential you point it at matters more than the package you pick:
// .mcp.json — wire integrations LAST, and read-only first{ "mcpServers": { "payments-db": { "command": "npx", // the reference Postgres server is archived; pick a maintained, // read-only server and point it at a read-only database role "args": ["-y", "<postgres-mcp-server>", "postgresql://readonly@localhost/payments"] } }}That readonly role is the seatbelt on the amplifier: it lets the agent inspect the real payments schema and write a migration that fits it, without handing it a way to mutate production while it is still learning the system. Add integrations last so there is a harness worth amplifying, and add them read-only first so a wrong guess costs you a bad diff to review, not a bad write to undo.
The two-lane flow: context saturation versus a scoped edit
The whole argument, in one picture: the same task, run two ways.
classes: {
task: {style: {fill: "#FFD93D"; stroke: "#D4B02A"; font-color: "#2C2C2A"}}
neutral: {style: {fill: "#4A90E2"; stroke: "#2C6FB0"; font-color: "#FFFFFF"}}
split: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
fail: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
good: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
}
direction: down
t: "Task: add refund support\nto the payments service" {class: task}
a1: "Single unbounded session" {class: neutral}
a2: "Reads 200+ files;\ngrep follows every reference" {class: neutral}
a3: "Early instructions decay\nunder everything it read" {class: neutral}
a4: "Architecturally wrong PR:\nwrong logger, wrong ORM, bare errors" {class: fail}
b1: "Exploration / editing split" {class: split}
b2: "Read-only explorer maps the\nsubsystem in throwaway context" {class: neutral}
b3: "Writes the findings file\npayments-refund-map.md" {class: neutral}
b4: "Fresh session loads only\nthe map + one example file" {class: neutral}
b5: "Scoped, convention-correct PR" {class: good}
t -> a1 -> a2 -> a3 -> a4
t -> b1 -> b2 -> b3 -> b4 -> b5
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, until recently, almost no one wrote it down. I will mark exactly where the evidence for it 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 holds it back. It forces an artificial, worse split of the work 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.
Over two model releases, the same line in CLAUDE.md goes from guardrail to drag:
classes: {
grey: {style: {fill: "#F5F5F5"; stroke: "#CCCCCC"; font-color: "#2C2C2A"}}
green: {style: {fill: "#6BCF7F"; stroke: "#3F9E52"; font-color: "#2C2C2A"}}
red: {style: {fill: "#E74C3C"; stroke: "#B03A2E"; font-color: "#FFFFFF"}}
purple: {style: {fill: "#7B68EE"; stroke: "#5A4BC4"; font-color: "#FFFFFF"}}
}
grid-rows: 1
grid-gap: 40
n: "Release N" {
label.near: top-center
grid-rows: 2
grid-columns: 1
grid-gap: 10
m: "Model loses the thread\nacross a multi-file edit" {class: grey}
r: "\"One file per change\"\nhelps: a guardrail" {class: green}
}
n1: "Release N+1" {
label.near: top-center
grid-rows: 2
grid-columns: 1
grid-gap: 10
m: "Model holds a coherent\nfive-file refactor" {class: grey}
r: "Same rule hurts:\nforces a worse split" {class: red}
}
audit: "After the release" {
label.near: top-center
grid-rows: 2
grid-columns: 1
grid-gap: 10
m: "Ask of each rule: does it\nstill protect against anything?" {class: purple}
r: "Delete the ones\nthat do not" {class: green}
}
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.
When this article first ran, this was a reasoned position rather than a citable fact. It now has a citation. Anthropic's guide to large codebases tells you to revisit instructions after major model releases, because "instructions that worked around an older model's limitation may become overhead once a newer model handles the case on its own. For example, a rule that forces single-file refactors can be deleted once the limitation is gone." That is the rule above, almost word for word. It is still guidance, not a benchmarked result, and the rest of the support is indirect. An empirical study of AI coding agents (arXiv:2511.04824, on refactorings in open-source Java projects) 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 note that longer instruction files reduce adherence tells you that harness configuration has real, sometimes negative, weight on behavior. None of this measures a rule built for a weak model holding back a strong one. Together, it makes that the safer assumption. A benchmark published since measures an adjacent effect rather than this one: a self-built harness lost between 20 and 36 points when only the executing model changed. That is coupling to the model that authored the harness, not a weak rule holding back a strong model, so it narrows the gap without closing it.
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? Chapter 2 of Harness Engineering, "Agent = Model + Harness, and the Trust Boundary," turns that review into an artifact for production LLM applications: a dated description of the model the harness was designed against, which any change to the model, the provider, the sampling parameters, or the prompt template invalidates, so the re-check is set off by the change instead of by someone remembering. Chapter 17 opens with what skipping it costs: a validator that had been correct for eleven months rejected every request the day after a model upgrade. 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.
- Instruction hierarchy first. A lean root
CLAUDE.md(pointers and gotchas, nothing the agent can infer) plus nestedCLAUDE.mdfiles that carry each module's local conventions - logger, data access, error handling - with afile:lineexample of each. This alone defeats most of Convention Inversion. - Scoped commands and exclusions. Per-directory test and lint commands so the agent runs the right suite. Rely on
.gitignorefor build output, and addReaddeny rules for committed generated and vendored files. Enforce secrets exclusion the same way, or with a hook..claudeignoreis not a supported control. - 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.
- 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.
- 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.
- 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.
Going deeper
The Harness Engineering series grew into a book, Harness Engineering for Production AI Systems. It is written for production LLM applications rather than coding agents, and it says so, but most of what this article asks of a coding-agent harness has a chapter there.
- Why the evidence comes from coding agents. Chapter 1, "The Demo Cliff Is Not a Model Problem," explains why almost every published result for harness work comes from coding agents: a coding agent has a test suite that returns a verdict on every run, and most applications have nothing like it.
- What the model sees, and where. Chapter 5, "Context Assembly as Managed Infrastructure," on budgeting a context window by priority and testing whether a constraint's position in it changes whether it is followed.
- Rules that outlive their model. Chapter 2, "Agent = Model + Harness, and the Trust Boundary," on dating your description of the model and re-checking it on every model change; Chapter 17, "Failure Modes of the Control Layer Itself," on why a control that has stopped working looks, in production, exactly like one that had nothing to stop.
- A real gate, not a hint. Chapter 9, "Gated Execution," on the check that sits after the model and before the action and fails closed, which is the job
permissions.denydoes for a path that an instruction can only ask the agent to avoid.
References
- Anthropic. Best practices for Claude Code. Claude Code Documentation. https://code.claude.com/docs/en/best-practices
- Anthropic. (2025, September 29). Effective context engineering for AI agents. Anthropic Engineering. https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
- Anthropic. Effective harnesses for long-running agents. Anthropic Engineering. https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents
- Anthropic. Set up Claude Code in a monorepo or large codebase. Claude Code Documentation. https://code.claude.com/docs/en/large-codebases
- Anthropic. Code intelligence plugins. Claude Code Documentation. https://code.claude.com/docs/en/plugins/code-intelligence
- Anthropic. Manage Claude's memory (CLAUDE.md). Claude Code Documentation. https://code.claude.com/docs/en/memory
- Anthropic. Context windows. Claude Platform Documentation. https://platform.claude.com/docs/en/build-with-claude/context-windows
- Anthropic. Create custom subagents. Claude Code Documentation. https://code.claude.com/docs/en/sub-agents
- Hong, K., Troynikov, A., & Huber, J. (2025, July 14). Context Rot: How Increasing Input Tokens Impacts LLM Performance. Chroma Technical Report. https://www.trychroma.com/research/context-rot
- Agentic Refactoring: An Empirical Study of AI Coding Agents. arXiv:2511.04824; published in ACM Transactions on Software Engineering and Methodology, https://doi.org/10.1145/3859242. https://arxiv.org/abs/2511.04824
- Kravcenko, V. (2026, March 3). Claude Code Doesn't Index Your Codebase. Here's What It Does Instead (quoting Boris Cherny and team; primary sources: Hacker News item 43164253 and Cherny on X). https://vadim.blog/claude-code-no-indexing/
- oraios. Serena - LSP-powered coding agent toolkit / MCP server. GitHub. https://github.com/oraios/serena
- TianPan. (2026, April 19). AI Coding Agents on Legacy Codebases: What Works and What Backfires. https://tianpan.co/blog/2026-04-19-ai-coding-agents-brownfield-legacy-code
- Duman, Y. (2025, December 9). Six Months of Agentic Coding in the Trenches: Lessons from a Brownfield Project. https://www.yduman.dev/posts/six-months-of-agentic-coding/
- Kumar, R. (2026). Harness Engineering for Production AI Systems. Chapter 2, "Agent = Model + Harness, and the Trust Boundary," and Chapter 5, "Context Assembly as Managed Infrastructure." https://harness.ranjankumar.in/
- Claburn, T. (2026, January 28). Claude Code reads .env files despite ignore rules. The Register. https://www.theregister.com/2026/01/28/claude_code_ai_secrets_files/
Related Articles
- Claude Code's Code Intelligence Usually Makes It Dumber
- The Ralph Loop and /goal: What Claude Code Actually Automated
- Agent Skills Are Not Prompts. They Are Production Knowledge Infrastructure.
- Claude Code Guide: Build Agentic Workflows with Commands, MCP, and Subagents






