Skip to content

Million+ LOC strategies for coding agents

Coding agents work in million-line codebases through selection, not capacity. Layered instruction files carry the architecture, and a code-intelligence MCP server finds symbols and callers. A read-only blast-radius report lists every affected file before any edit. Each change then lands in small slices, checked by type, test and residue gates rather than by a human reading the whole diff.

You inherited a 1.8-million-line monolith whose architects left two years ago, and your first ticket renames User.role. The agent edits three files in packages/web, reports success, and two hours after the deploy the billing-worker service throws, because it reads the same field through a shared event schema the agent never opened. The model was not careless; it never saw the file that mattered.

This page is for developers who make changes in a very large repository, and for tech leads who set up the instruction files, MCP servers and gates the whole team relies on.

What you get from this large-codebase workflow

Section titled “What you get from this large-codebase workflow”
  • A three-layer instruction setup (root, package, on-demand) for Claude Code, Codex and Cursor, including the loading rules that decide whether the agent ever sees a package’s instructions
  • A choice of code-intelligence MCP server (Serena, codebase-memory-mcp or Claude Context) with verified install commands and no credentials in config files
  • Four copy-paste prompts: architecture map, rename blast radius, single-module dependency graph, and one-slice execution
  • A merge gate that proves a 60-file change is complete without anyone reading all 60 files
  • Recovery steps for stale indexes, invented paths and counts, context overflow and changes that pass alone but break integration

Why doesn’t a bigger context window fix a million-line repository?

Section titled “Why doesn’t a bigger context window fix a million-line repository?”

A multi-million-line repository does not fit in any current context window (sizes are on the models hub), and filling most of a window with code lowers answer quality long before it overflows. What fails in the opening story is selection: the one file that mattered was never loaded. So you design what loads, in layers:

LayerHoldsWhere it livesWhen it loads
ArchitectureModule map, dependency direction, “always check the shared schema” rulesRoot CLAUDE.md / AGENTS.md / always-on Cursor ruleEvery session
DomainOne package’s invariants and contractsPackage-level instruction file or path-scoped ruleWhen the agent works in that package
Implementation and referenceThe files being changed, and a well-written example to imitateFound by search, pulled in by the agentOn demand, per task

The architecture layer is the one you write once and stop paying for. Keep it short: every line loads in every session. Pruning CLAUDE.md and AGENTS.md shows how to measure which lines earn their place.

Encode the architecture layer once per repository

Section titled “Encode the architecture layer once per repository”

The three tools load nested instructions differently, and the difference decides whether a package’s rules are ever read.

Claude Code loads every CLAUDE.md from the working directory upward at launch. A CLAUDE.md in a subdirectory loads on demand, when Claude reads a file in that subdirectory. For rules that depend on the file rather than the folder, use a path-scoped rule in .claude/rules/:

---
paths:
- "services/payment/**"
---
# Payment invariants
- Amounts are integer cents; never floats.
- Every mutation takes an idempotency key.
- Never log a full card number.

The root CLAUDE.md holds the architecture layer:

# Architecture (1.8M lines, event-driven)
- Services own their schemas; shared types live in packages/shared-types/.
- Inter-service traffic goes through packages/event-bus/. A field read by
another service is also read from event payloads: search events/ too.
- Before any cross-package change, produce a blast-radius report (see
docs/architecture/map.md) and wait for approval.

Claude Code (latest channel, v2.1.277 and later) reads AGENTS.md when a project has no CLAUDE.md, so a repository shared with Codex users can keep one file.

Give the agent code intelligence instead of grep loops

Section titled “Give the agent code intelligence instead of grep loops”

Without help, an agent explores a large repository with search and read: dozens of calls, each adding file content to the window. A code-intelligence MCP server answers “who calls this?” in one call. Three maintained options, with different trade-offs:

ServerHow it finds codeWhere your code goesNeedsPopularity (read 2026-09-26)
Serena (Oraios)Language servers: symbols, references, symbol-level editsStays localuv, a language server per language★29.8k; PyPI serena-agent 1.7.0
codebase-memory-mcp (DeusData)Tree-sitter knowledge graph with call-path tracing (trace_path)Stays localA native binary★44.9k; 0.11.0 on npm and PyPI
Claude Context (Zilliz)Hybrid BM25 and vector search over chunksEmbeddings, and with a hosted provider the code chunks, leave the machine unless you run Milvus and Ollama locallyA Milvus or Zilliz vector database and an embedding API★12.6k; npm @zilliz/claude-context-mcp 0.1.15, last published 2026-06-22

Start with Serena when your languages have good language servers and code must stay on your machines. It resolves a rename through the language server, so it finds the call sites text search misses. Choose Claude Context when the question is conceptual (“where do we retry failed charges?”) and naming is inconsistent. The full comparison is on MCP servers that understand your codebase.

Terminal window
# Terminal: install Serena, then register it for every project (Claude Code 2.1.283, `latest` channel)
uv tool install -p 3.13 serena-agent
serena init
claude mcp add --scope user serena -- serena start-mcp-server --context claude-code --project-from-cwd

For codebase-memory-mcp, install the binary from npm and register it:

Terminal window
# Terminal: codebase-memory-mcp 0.11.0 (registration derived from the README's JSON config)
npm install -g codebase-memory-mcp
claude mcp add codebase-memory -- codebase-memory-mcp

For Claude Context, commit a project .mcp.json that reads keys from the environment, so no key lands in the repository. Pin the package version: an unpinned @latest in a committed file runs whatever is published next on every teammate’s machine. Bump the pin deliberately, after reading the release.

{
"mcpServers": {
"claude-context": {
"command": "npx",
"args": ["-y", "@zilliz/claude-context-mcp@0.1.15"],
"env": {
"OPENAI_API_KEY": "${OPENAI_API_KEY}",
"MILVUS_ADDRESS": "${MILVUS_ADDRESS}",
"MILVUS_TOKEN": "${MILVUS_TOKEN}"
}
}
}
}

Recon you throw away is recon you pay for again next week. Run it once, read-only, and write the result to docs/architecture/map.md, which the root instruction file points to.

Terminal window
# Terminal, repository root. dontAsk runs reads, read-only commands and your
# allow-listed tools; anything that would prompt is denied. The prompt also
# says "Do not modify any file". Write to map.new.md so the old map survives.
mkdir -p docs/architecture
claude -p --permission-mode dontAsk "$(cat prompts/architecture-map.md)" > docs/architecture/map.new.md
git diff --no-index docs/architecture/map.md docs/architecture/map.new.md
mv docs/architecture/map.new.md docs/architecture/map.md # after you review the diff

In an interactive session, paste the prompt after /plan. The built-in Explore subagent reads files in its own context and returns only the summary, which keeps your main window small.

Save the prompt as prompts/architecture-map.md for the headless commands. On the first run there is no old map, so the diff shows the whole file. Regenerate the map after a large merge, and review its diff before you replace the old one: a change in dependency direction is an architecture decision someone should have made on purpose.

Produce a blast-radius report before any edit

Section titled “Produce a blast-radius report before any edit”

The highest-leverage move in a large repository is making the agent find and report the affected files before it edits anything. The read-only pass is cheap compared with a refactor that breaks a service nobody listed. The report is also the acceptance checklist for the merge gate below, so ask for a machine-readable file list at the end.

A change centred on one class needs a graph rather than a list. This version catches listeners and subscribers that react to the class’s events without importing it:

If the report misses a service you know exists, stop. Either the index is stale, or ignore rules hide the folder. Fix that before you trust the next report.

Never hand a large repository an open-ended task such as “refactor the auth system”. The agent fans out, loses the thread, and you get a 40-file diff nobody can review. Order the work so the compiler finds what the report missed:

  1. Approve the blast-radius report. Add any consumer you know it missed, save the FILES block as blast-radius.txt, and commit it on the feature branch before you fan out. New worktrees get only tracked files, and Claude Code worktrees start from the remote default branch unless you set worktree.baseRef to "head", as the fan-out tabs below show.

  2. Change shared contracts first. Types, interfaces, event schemas and migrations go first. The type errors they cause are a second, independent list of affected files.

  3. Change one package per turn. Each turn ends with that package’s tests and the repository-wide type check passing, not with “done”.

  4. Run the consumers’ tests, not only your own. Your build system’s affected-target command is the fastest way to find them. Monorepo workflows covers Nx and Turborepo project graphs.

  5. Run the merge gate below before you open the pull request.

When the slices are mechanical and independent, such as the same rename in 20 packages, fan them out instead of running them one by one:

The bundled /batch skill “decomposes the work into 5 to 30 independent units, and presents a plan”, then runs one background subagent per unit in its own worktree once you approve. Name the units yourself, so it does not invent a split.

Worktrees branch from the remote default branch by default. Set "worktree": { "baseRef": "head" } in .claude/settings.json first, so every unit starts from your feature branch with blast-radius.txt and the new contracts. blast-radius.txt must be committed, because new worktrees get only tracked files:

/batch rename User.role to User.accountType, one unit per package listed in blast-radius.txt. Each unit installs dependencies first, changes only files from blast-radius.txt inside its package, and is done when that package's tests and the repository type check pass.

Each subagent publishes its own change, so you get one change per package. Merge them onto the feature branch and run the merge gate on the combined branch, not on each unit.

Size the fan-out by review capacity, not by how many agents you can start. A migration that runs for weeks, with characterization tests and cut-over evidence, is a program: see legacy modernization with agents.

How do you verify a 60-file change without reading every line?

Section titled “How do you verify a 60-file change without reading every line?”

You do not approve a large change by reading it. You approve it because a set of checks, each able to fail, passed. For a cross-package change, these are the checks that matter:

GateWhat it provesFails when
Repository-wide type checkEvery compiled consumer agrees with the new contractA call site still passes the old shape
Affected tests across all consumersBehaviour of every dependent package still holdsA downstream service relied on the old field
Zero-residue searchNo reference to the old name survivesA string key, SQL column or event field was skipped
Report-versus-diff reconciliationEvery file in the report was changed or explicitly excusedThe agent quietly skipped a consumer
Contract or schema tests for events and APIsConsumers outside the type system still parse the payloadbilling-worker reads role from JSON
Fitness functionsDependency direction and layering are intactThe agent imported across a forbidden boundary
Review agent passA second model looked for correctness bugs in the diffLogic errors the checks above cannot see

The residue and reconciliation checks are two commands you can put in CI:

Terminal window
# Terminal or CI, on the feature branch; paths in blast-radius.txt are repository-relative
# 1. Zero residue: narrow the pattern so it matches only the renamed field
rg -n '\.role\b|"role"' packages/ services/ --glob '!**/migrations/**' && exit 1
# 2. Every file the report named was changed, or is excused in excused.txt
touch excused.txt
git diff --name-only origin/main...HEAD | sort > changed.txt
sort -u blast-radius.txt | comm -23 - changed.txt | grep -vxFf excused.txt && exit 1
echo "blast radius covered"

For the review agent, run /code-review in Claude Code, codex review --base main in Codex, or Bugbot on the pull request in Cursor. Put the report, the gate output and the excused list in the pull request as its evidence bundle. Sign-off belongs to the owners of each touched package, through CODEOWNERS. Their job is to check that the excused list and the report are plausible for their service, not to read every line.

In a long session, the conversation itself becomes stale context: the agent keeps “remembering” a file layout from before the last rebase. Reset when you switch tasks.

/clear starts a new conversation and keeps project memory. /compact Focus on the accountType rename; drop the CSS work keeps the thread but summarizes it. /context shows what fills the window before a large task. If you corrected the agent twice on the same point, /clear and restart with a sharper prompt.

What breaks when agents work in a million-line codebase?

Section titled “What breaks when agents work in a million-line codebase?”

The index is stale after a large merge. The agent cites files that moved or no longer exist. Re-index after large merges, rebases and code generation; Claude Context has a clear_index tool for a full rebuild. For Claude Code and Codex, which read files live, the stale artifact is usually the architecture map or the root instruction file: regenerate the map and diff it.

The agent invents paths and imports. The architecture layer is too thin, or the package file never loaded (in Codex, check the directory you started in: files below it are read only if the model goes looking). Tell the agent to locate the definition first: “Find where UserService is defined, then write the import.” Treat any path it gives you as a claim to check.

The agent invents precise counts. Ask “how many callbacks are there?” and you may get a confident, exact-looking number it never computed. Require the command: “Report the count with the rg command you ran.” Trust a count only when you can rerun it.

The context window fills with one file. An 8,000-line generated client can crowd out everything else. Point the agent at a symbol, not the file, or have it read a line range. Keep generated code out of the semantic index with .cursorignore or your server’s ignore rules.

Each slice passes, the integration fails. A consumer outside the type system, such as an event subscriber or a SQL view, broke. Add a contract test for that boundary, add the consumer to the architecture map’s “reads without importing” list, and rerun the reconciliation gate.

The agent edits a re-export instead of the source. It lost the dependency direction. Rerun the dependency-graph prompt and name the source-of-truth file explicitly in the slice prompt.

Parallel slices collide. Two agents edited a shared utility that the split missed. Give shared files to one slice only, and merge that slice first.

The MCP server does not connect. Check claude mcp list or codex mcp list, and in Claude Code /mcp. A missing ${VAR} in .mcp.json shows as a warning naming the variable. For Serena, run serena start-mcp-server by hand to see the language-server error.