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:
| Layer | Holds | Where it lives | When it loads |
|---|---|---|---|
| Architecture | Module map, dependency direction, “always check the shared schema” rules | Root CLAUDE.md / AGENTS.md / always-on Cursor rule | Every session |
| Domain | One package’s invariants and contracts | Package-level instruction file or path-scoped rule | When the agent works in that package |
| Implementation and reference | The files being changed, and a well-written example to imitate | Found by search, pulled in by the agent | On 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.
Codex automatically loads only the AGENTS.md files from the project root (the directory holding .git) down to the directory you started it in (0.157.1). Its default instructions tell the model to look for AGENTS.md in the subdirectories it works in, but that depends on the model doing so. Start Codex in the package when its rules must apply:
# Terminal: start in the package so its AGENTS.md is loadedcodex -C services/paymentWith -C, the package becomes the writable root. For a change that must also touch shared contracts, add --add-dir packages/shared-types or start at the repository root and name the package file in the prompt.
The combined instructions are capped by project_doc_max_bytes, 32 KiB by default (Codex 0.157.1 source, agents_md.rs and config/mod.rs). A long root file silently crowds out the package file. Put the same architecture layer as in the Claude Code tab in the root AGENTS.md, and the payment invariants in services/payment/AGENTS.md. Since Codex 0.150.0, untrusted projects do not supply project-level AGENTS.md, so trust the repository first.
Put the architecture layer in a project rule that always applies, and the payment invariants in a second rule scoped to services/payment/**. Cursor’s Rules documentation (checked 2026-08-28) has the current file format and scoping options. The rule bodies are the same Markdown as in the Claude Code tab.
Cursor also indexes the workspace for its own semantic search. List generated output, lockfiles and snapshots in a committed .cursorignore (.gitignore syntax), or they dominate search results in a large repository. .cursorignore blocks agent reads (@cursor/sdk 1.0.32); whether it also removes paths from the semantic index is unverified, see codebase indexing.
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:
| Server | How it finds code | Where your code goes | Needs | Popularity (read 2026-09-26) |
|---|---|---|---|---|
| Serena (Oraios) | Language servers: symbols, references, symbol-level edits | Stays local | uv, 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 local | A native binary | ★44.9k; 0.11.0 on npm and PyPI |
| Claude Context (Zilliz) | Hybrid BM25 and vector search over chunks | Embeddings, and with a hosted provider the code chunks, leave the machine unless you run Milvus and Ollama locally | A 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: install Serena, then register it for every project (Claude Code 2.1.283, `latest` channel)uv tool install -p 3.13 serena-agentserena initclaude mcp add --scope user serena -- serena start-mcp-server --context claude-code --project-from-cwdFor codebase-memory-mcp, install the binary from npm and register it:
# Terminal: codebase-memory-mcp 0.11.0 (registration derived from the README's JSON config)npm install -g codebase-memory-mcpclaude mcp add codebase-memory -- codebase-memory-mcpFor 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}" } } }}# Terminal: install Serena, then let it write its Codex config (Codex CLI 0.157.1)uv tool install -p 3.13 serena-agentserena initserena setup codexFor codebase-memory-mcp, install the binary from npm and register it:
# Terminal: codebase-memory-mcp 0.11.0 (registration derived from the README's JSON config)npm install -g codebase-memory-mcpcodex mcp add codebase-memory -- codebase-memory-mcpFor Claude Context, add this to ~/.codex/config.toml. env_vars forwards the named variables from your shell, so the keys never appear in the file. The version is pinned for the same reason as in the Claude Code tab:
[mcp_servers.claude-context]command = "npx"args = ["-y", "@zilliz/claude-context-mcp@0.1.15"]env_vars = ["OPENAI_API_KEY", "MILVUS_ADDRESS", "MILVUS_TOKEN"]startup_timeout_ms = 20000Add Serena as a stdio server in Cursor’s MCP settings with command serena and arguments start-mcp-server --context ide, the context Serena’s README recommends for IDEs. That server starts with no project: ask the agent to “Activate the current dir as project using serena” at the start of each session, or add --project <path> for a per-workspace setup. For Claude Context, use the mcpServers JSON from the Claude Code tab. Read the keys from your environment with the syntax on Cursor’s MCP page, rather than pasting them into the file.
Map the system before you take the ticket
Section titled “Map the system before you take the ticket”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, 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/architectureclaude -p --permission-mode dontAsk "$(cat prompts/architecture-map.md)" > docs/architecture/map.new.mdgit diff --no-index docs/architecture/map.md docs/architecture/map.new.mdmv docs/architecture/map.new.md docs/architecture/map.md # after you review the diffIn 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.
# Terminal, repository root: read-only profile, final answer written to a new filemkdir -p docs/architecturecodex exec -c default_permissions=":read-only" -o docs/architecture/map.new.md "$(cat prompts/architecture-map.md)"git diff --no-index docs/architecture/map.md docs/architecture/map.new.mdmv docs/architecture/map.new.md docs/architecture/map.md # after you review the diffPaste the prompt below in Plan Mode, which “creates detailed implementation plans before writing any code”. Save the answer as docs/architecture/map.md and reference it from the always-on rule.
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.
Change the code in verified slices
Section titled “Change the code in verified slices”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:
-
Approve the blast-radius report. Add any consumer you know it missed, save the
FILESblock asblast-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 setworktree.baseRefto"head", as the fan-out tabs below show. -
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.
-
Change one package per turn. Each turn ends with that package’s tests and the repository-wide type check passing, not with “done”.
-
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.
-
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.
Run one non-interactive job per package, each in its own worktree. codex exec --worktree checks out your current HEAD, so commit blast-radius.txt and the contract changes first: new worktrees get only tracked files.
# Terminal, repository root (Codex CLI 0.157.1; the :workspace profile is beta)mkdir -p runsfor pkg in packages/api packages/web services/billing-worker; do codex exec --worktree -c default_permissions=":workspace" -o "$PWD/runs/$(basename "$pkg").md" \ "Install dependencies first. Rename User.role to User.accountType in $pkg only, limited to files listed in blast-radius.txt. Done means the package tests and the repository type check pass. Then commit the change." &donewaitEach run leaves its own managed worktree; list it with git worktree list and cherry-pick or merge each commit onto the feature branch before you run the merge gate.
Run one agent per package in its own worktree, or start one Cloud Agent per package, each with the one-slice prompt above and its package name.
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:
| Gate | What it proves | Fails when |
|---|---|---|
| Repository-wide type check | Every compiled consumer agrees with the new contract | A call site still passes the old shape |
| Affected tests across all consumers | Behaviour of every dependent package still holds | A downstream service relied on the old field |
| Zero-residue search | No reference to the old name survives | A string key, SQL column or event field was skipped |
| Report-versus-diff reconciliation | Every file in the report was changed or explicitly excused | The agent quietly skipped a consumer |
| Contract or schema tests for events and APIs | Consumers outside the type system still parse the payload | billing-worker reads role from JSON |
| Fitness functions | Dependency direction and layering are intact | The agent imported across a forbidden boundary |
| Review agent pass | A second model looked for correctness bugs in the diff | Logic errors the checks above cannot see |
The residue and reconciliation checks are two commands you can put in CI:
# 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 fieldrg -n '\.role\b|"role"' packages/ services/ --glob '!**/migrations/**' && exit 1
# 2. Every file the report named was changed, or is excused in excused.txttouch excused.txtgit diff --name-only origin/main...HEAD | sort > changed.txtsort -u blast-radius.txt | comm -23 - changed.txt | grep -vxFf excused.txt && exit 1echo "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.
Keep context fresh between tasks
Section titled “Keep context fresh between tasks”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.
/new starts a new chat in the same session, and /clear also clears the terminal. /compact summarizes the conversation, and /status shows token usage. To pick up a different package’s AGENTS.md, start a new session in that package with codex -C <dir>.
Start a new chat for each distinct task, so the agent reasons only about the job in front of it, and re-attach the architecture map if the always-on rule does not already load it.
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.
Where to go next with large codebases
Section titled “Where to go next with large codebases”- Monorepo workflows: project-graph MCP servers and cascade refactors across packages
- Legacy modernization with agents: characterization tests, slices and cut-over evidence for multi-week migrations
- MCP servers that understand your codebase: Serena, codebase-memory-mcp, Claude Context and Repomix compared
- Codebase indexing and context windows: how search and window size interact
- Architecture fitness functions: make dependency rules fail the build instead of relying on review
- Distributed systems development: the same discipline across service boundaries