Semantic, not textual
With a vector index, “find all authentication flows” surfaces OAuth, JWT, and session code even when none of them share a keyword.
Million-line codebases exceed every context window, so AI-assisted work in them depends on structure rather than capacity: a layered context pyramid instead of loading the repo, semantic search instead of grep, a blast-radius report before any edit, and incremental changes verified module by module. Cursor, Claude Code, and Codex each need different indexing and context setup.
You inherited a 1.8-million-line monolith. The original architects left two years ago, the docs describe a system that no longer exists, and your first ticket renames a field on User. The agent confidently edits three files in packages/web, declares victory, and two hours after you ship the billing-worker service throws in production, because it read the same field from a shared schema the agent never opened. The model was not wrong because it is dumb. It was wrong because it never saw the file that mattered.
Current flagships support 1M-token context windows (Claude Fable 5, Opus 5, Sonnet 5, Gemini 3.1 Pro) and a multi-million-line codebase still will not fit. The answer is not more capacity, it is better selection — and that is exactly where AI earns its place at this scale.
Semantic, not textual
With a vector index, “find all authentication flows” surfaces OAuth, JWT, and session code even when none of them share a keyword.
Dependency tracing
The AI follows imports and call sites across module boundaries far faster than you can click through “find usages.”
Characterization tests
For undocumented legacy code, the AI drafts tests that pin current behavior so you can refactor without fear.
You stay the architect
The AI does the mechanical scanning and boilerplate. You make the domain and architecture calls it cannot.
Think of context as four layers, loaded in decreasing permanence:
The architecture layer is the one you encode once and stop paying for. Each tool has a file for it, and the shape of that file is what separates an agent that reasons about the system from one that guesses from three open tabs.
Cursor’s codebase indexing supplies part of the architecture layer automatically. Pin the rest in a Project Rule so it applies without being re-typed:
# .cursor/rules/architecture.mdc (alwaysApply: true)Large-scale payment processing platform, 1.8M LOC.Key modules:- /src/payments/ - Payment processing (Stripe, PayPal, internal ledger)- /src/accounts/ - User account management and KYC- /src/notifications/ - Event-driven notification system- /src/shared/ - Shared types, utilities, and base classes
When modifying any module, always check:1. The module's public API in its index.ts barrel export2. Integration tests in /tests/integration/{module-name}/3. The event contracts in /src/shared/events/Scope the domain layer with a glob-bound rule next to the code it governs:
# .cursor/rules/payment.mdc (glob: services/payment/**)When working with payment code:- All monetary amounts are integer cents — never floats- Mutations require an idempotency key- Never log full card numbers (PCI)- Add audit logging for every state transitionPull specific context in with @file and @folder. For cross-cutting changes, reference the shared contract first: @src/shared/types/payment.ts before touching any payment module. For straightforward projects, a root AGENTS.md works as a simpler alternative to structured rules.
Claude Code reads a hierarchy of CLAUDE.md files, each directory’s layering onto its parents — which maps onto the pyramid directly:
# /CLAUDE.md (root - architecture layer)Monorepo with 1.8M LOC. Key architectural decisions:- Event-driven architecture using RabbitMQ- Each service owns its database schema- Shared types live in /packages/shared-types/- All inter-service communication goes through /packages/event-bus/
# /packages/payments/CLAUDE.md (domain layer)Payment service handles Stripe and PayPal integrations.Never modify PaymentProcessor directly - extend via strategy pattern.All new payment methods must implement IPaymentStrategy interface.Then switch cleanly between unrelated areas with /clear and /add-dir, so the domain layer swaps while the architecture layer stays:
/clear/add-dir services/paymentAnalyze the payment-processing flow.
/clear/add-dir services/usersReview the authentication implementation.Codex reads AGENTS.md from the repo root and from any subdirectory, so the architecture layer goes at the root and the domain layer next to the code:
# AGENTS.md (root)Large monorepo navigation rules:- Always run `find . -name "*.ts" -path "*/payments/*" | head -20` to orient before modifying payment code- Check /docs/architecture/ for system design documents before cross-service changes- Use git log --oneline -20 on target files to understand recent change patterns
When making changes that span multiple packages:1. List all affected packages first2. Check each package's README for modification guidelines3. Run the package's test suite after each change# services/payment/AGENTS.md (domain layer)This service handles all payment processing.- Amounts are integer cents to avoid floating-point error- Idempotency keys required on all transactions- PCI: never log full card numbersSeparate git worktrees enable parallel Codex exploration without conflicts; managed worktrees are an optional ChatGPT desktop choice, not a property of every task.
Text search fails at scale because related code rarely shares vocabulary. A semantic index built on vector embeddings fixes that: it answers “where is payment processing handled?” without the file being open. The maintained server is Zilliz Claude Context (@zilliz/claude-context-mcp — previously published as code-context). MCP setup is nearly identical across all three tools; only the registration command differs.
Add to ~/.cursor/mcp.json:
{ "mcpServers": { "claude-context": { "command": "npx", "args": ["-y", "@zilliz/claude-context-mcp@latest"], "env": { "EMBEDDING_PROVIDER": "OpenAI", "OPENAI_API_KEY": "your-api-key", "MILVUS_TOKEN": "your-zilliz-key" } } }}claude mcp add claude-context \ -e OPENAI_API_KEY=your-api-key \ -e MILVUS_TOKEN=your-zilliz-key \ -- npx -y @zilliz/claude-context-mcp@latestcodex mcp add claude-context \ --env OPENAI_API_KEY=your-api-key \ --env MILVUS_TOKEN=your-zilliz-key \ -- npx -y @zilliz/claude-context-mcp@latestOr add it directly to ~/.codex/config.toml:
[mcp_servers.claude-context]command = "npx"args = ["-y", "@zilliz/claude-context-mcp@latest"]env = { EMBEDDING_PROVIDER = "OpenAI", OPENAI_API_KEY = "your-api-key", MILVUS_TOKEN = "your-zilliz-key" }Once indexed, you ask for concepts and the server returns the relevant files regardless of naming. For sensitive codebases that can’t reach a cloud embedding API, LuotoCompany/cursor-local-indexing runs an on-premise ChromaDB index and exposes it over a local SSE endpoint:
Add to ~/.cursor/mcp.json:
{ "mcpServers": { "workspace-code-search": { "url": "http://localhost:8978/sse" } }}claude mcp add --transport sse workspace-code-search http://localhost:8978/ssecodex mcp add workspace-code-search --url http://localhost:8978/sseThis keeps source code on your own infrastructure — the right call for financial services, healthcare, or defense work where code can’t leave the network.
An external vector index is one half; the other half is what the tool itself sees natively, and that differs sharply between the three.
Cursor indexes the workspace automatically and computes embeddings for semantic search. Keep the index lean with ignore files:
.cursorignore blocks files from indexing and from agent access (use for secrets, node_modules/, build output)..cursorindexingignore excludes files from the index only — they stay reachable via explicit @-mention. Use it for large generated files (lockfiles, dist/, snapshots) that pollute search results.dist/**/*.snappnpm-lock.yamlThen scope context with symbols, not whole files: @accountType (a symbol) gives a tighter, less noisy context than @user-service.ts (a 2,000-line file).
Claude Code doesn’t pre-index; it explores on demand with Grep/Glob and reads files as needed. Your job is to give it a durable map and watch the window:
/init once to generate a CLAUDE.md that records the monorepo layout, package boundaries, and build/test commands. This is the context it can’t infer from a cold start./context to see what’s consuming the window before a big task — if shared schema and three services already fill it, you’re about to overflow..claude/agents/) so the file-by-file grep runs in an isolated context and only the summary returns to your main thread.Codex reads AGENTS.md at the repo root (and per-package) as its persistent project context — document the workspace layout and the “always check the shared schema” rule there so it survives every new thread.
Inside the TUI, /init scaffolds an AGENTS.md from your codebase. For changes that touch many packages, run the work in a worktree (a separate checkout per thread) so a large refactor is isolated from your main working tree and easy to discard if the blast radius turns out bigger than expected.
Where do you start? Top-down. Get the AI to build a mental model before you touch anything, then drill into the area your ticket actually concerns.
Cursor’s Agent self-gathers context from the indexed codebase — just describe what you want. Use @Folders to scope a question to one area and @Code to point at a specific snippet:
@Folders services/authExplain the authentication and authorization architecture: where tokensare issued, how refresh works, and which services validate them.For a precise reference, select a function in the editor and add it with @Code before asking the agent to trace its call sites.
Scope the session to the directory you care about with the --add-dir flag at startup (or /add-dir <path> mid-session), then ask broad-to-narrow. Use @-path mentions to pull a specific file into context:
Analyze this codebase and build a mental model of the system architecture.Cover: core business domains, service boundaries, data-flow patterns, andexternal dependencies. Present it as an overview for a new senior engineer.Then drill in with semantic search via the MCP server:
Using claude-context, find all payment-processing flows. I need entrypoints, state management during processing, external provider integration,and the retry/error-handling logic. Reference @services/payment as you go.Drop an AGENTS.md at the repo root describing the domains and conventions, then run /init inside the TUI to have Codex bootstrap it. For a large refactor, work in a dedicated git worktree so the exploration never touches your main checkout:
Map this codebase top-down: business domains, service boundaries, dataflow, and external dependencies. Then locate the payment-processing flowand summarize its entry points and retry logic.Recon you throw away is recon you pay for again next week. Write it to a file the tool re-reads on every future session — that file is your architecture layer, and keeping it current is cheaper than re-deriving it.
Where it lands differs per tool, and the destination is what makes it automatic rather than something you remember to @-mention:
Save it to .cursor/architecture.md and reference it with @.cursor/architecture.md, or point an always-on Project Rule at it so every chat starts with it loaded.
Save it to /docs/architecture-summary.md and link it from the root CLAUDE.md. You can generate it headlessly, outside a session:
claude "Analyze the entire /src directory structure and generatean architecture summary. For each package in /packages/:- What it does (one line)- Its public exports- Which other packages it depends on- Its test coverage statusSave to /docs/architecture-summary.md"Save it to /docs/architecture-map.md and reference it from the root AGENTS.md. A cloud task with full repository access is a good place to run the initial deep pass, since it can read the whole tree without competing with your local session for context.
The biggest mistake with large codebases is loading everything at once. Your assistant doesn’t need all 1.8 million lines — it needs the right slice at the right moment. Think of it as zooming on a map: continent, country, city, street.
Domain level (10,000 ft)
What are the main bounded contexts in this system, and how do the payment,user, and inventory domains interact?Service level (1,000 ft)
Within the payment domain, explain the service architecture and the mainAPIs each service exposes.Component level (100 ft)
Show me how PaymentProcessor handles credit-card transactions and what itsretry strategy is for failed charges.Implementation level (ground)
In PaymentProcessor.processCard(), why is there a 30-second timeout, and isthe synchronized block safe to remove?The single highest-leverage move in a large repo is to make the agent find and report the affected files before it edits anything. It catches the cross-package dependency the agent would otherwise miss, and it is cheap: a read-only pass costs a fraction of a botched refactor.
Two shapes of that pass, for two different changes. The first is for a change that spreads by name across packages — a rename, a field, a config key — and its output is a ranked table you review against:
If the report misses a service you know exists, your index is incomplete or its ignore rules are too aggressive — fix that before proceeding. Keep the report open as your review artifact and check files off as the change lands.
The second is for a change centred on one class or module. It asks for a graph rather than a list, and it is the version that catches the consumers a name search cannot see: event listeners and message-queue subscribers that never import the symbol at all.
Never hand a large repo an open-ended task like “refactor the entire authentication system.” The model fans out, loses the thread halfway, and you get a 40-file diff you can’t review. Make it emit a checklist first, then execute one item per turn.
List every file that must change
Get the list before a single line is written, and review it against your own understanding of the system.
Modify shared interfaces first
Start with type definitions, interfaces, and contracts. Those changes propagate compile errors that reveal the hidden dependencies the report missed.
Update implementations one module at a time
Modify each consuming module independently, and run that module’s tests before moving to the next.
Run integration tests after each module
Do not wait until all modules are updated. Catch integration failures while the diff is still small enough to read.
Verify across the cut
Run the full suite, type-check the whole codebase, and review the complete diff before committing.
Once the plan is approved, drive it one item at a time and name the exact symbol, so the agent stays on target instead of re-deriving scope:
When the phases are obvious from the shape of the change — an interface first, then its consumers — you can skip the separate planning turn and bake them into a single prompt with explicit stops. This is the compact form of the same discipline:
For the full planning discipline behind this loop, see PRD to Plan to Todo.
Refactoring a million-line codebase is like renovating a hospital while surgery continues — you can’t shut everything down. The pattern that works: discover, template, migrate in small batches, verify.
Take a Node.js codebase still riddled with error-first callbacks. Manual migration to async/await would take months. Instead, have the AI categorize the work by risk, then generate one reusable transformation per category:
// Before — error-first callbackfunction loadUser(id, callback) { db.query('SELECT * FROM users WHERE id = ?', [id], (err, rows) => { if (err) return callback(err); callback(null, rows[0]); });}
// After — async, with a backward-compatible callback shimasync function loadUser(id, callback) { try { const rows = await db.query('SELECT * FROM users WHERE id = ?', [id]); if (callback) return callback(null, rows[0]); return rows[0]; } catch (err) { if (callback) return callback(err); throw err; }}The shim lets callers migrate on their own schedule. Apply the transformation directory by directory, run the existing tests after each batch, and track progress — never transform the whole tree in one pass.
For a large effort split across a team, have the AI partition the work to minimize cross-team conflicts, then keep the branches honest:
Partition by dependency boundaries
Analyze module dependencies and propose how to split this refactor acrossfour developers so their territories barely overlap. Flag any shared filesthat two teams would both need to edit.Branch per territory
git checkout -b refactor/user-servicesgit checkout -b refactor/payment-servicesgit checkout -b refactor/shared-utilsDetect collisions early
Review the diffs across all refactor/* branches and identify conflictingor breaking changes between teams before we attempt to merge.Every large codebase has archaeological layers — code from different eras and philosophies, some of it predating the team. The classic horror: a 15,000-line stored procedure no one understands that still processes real money daily.
The strangler-fig pattern lets you modernize without a rewrite: wrap the legacy code behind a clean interface, then extract pieces one at a time while running old and new in parallel until you trust the new path.
Not every legacy change deserves a facade. When you only need to add something to old code, give the AI a Rosetta Stone instead: the most recent, well-written module that follows current conventions, as the reference pattern to imitate. This keeps the diff small where the strangler-fig prompt deliberately builds a new surface.
When documentation doesn’t exist, tests become the documentation. Ask the AI to write characterization tests that pin current behavior — including the weird parts — so any future change that alters output fails loudly:
describe('Legacy OrderProcessor — current behavior', () => { it('returns status code 1 on a standard single-item order', async () => { const result = await processOrder({ customerId: 123, items: [{ sku: 'WIDGET-1', quantity: 1 }], }); expect(result.status).toBe(1); // 1 = success (undocumented magic number) expect(result.orderId).toMatch(/^ORD-\d{8}$/); });
it('returns -99 when inventory is unavailable', async () => { const result = await processOrder({ customerId: 123, items: [{ sku: 'OUT-OF-STOCK', quantity: 1 }], }); expect(result.status).toBe(-99); // -99 = inventory error });});In a million-line codebase, different teams own different territory. The hard part is making a change that crosses a boundary without breaking someone else. The blast-radius prompts above stop at a report; this one goes further and asks for the migration path, because a breaking change you cannot stage is a breaking change you cannot ship.
Pair this with auto-generated contracts. Ask the AI to produce an OpenAPI spec and event schemas for a service another team consumes — that turns “go read our code” into a stable boundary they can integrate against without spelunking through your internals.
In a long session the conversation history itself becomes stale context — the agent keeps “remembering” the bug you fixed an hour ago. Reset deliberately when you switch tasks. The mechanics differ per tool:
Start a new chat for each distinct task (new feature, new bug). Cursor keeps each chat’s context separate, so a fresh chat means the agent reasons only about the job in front of it. Use checkpoints to roll back a chat if an exploratory edit goes sideways.
Run /clear when moving between unrelated tasks to wipe the context window entirely. When you want to keep the thread but trim noise, use /compact <instructions> — e.g. /compact Focus on the JWT migration, drop the earlier CSS work. If you’ve corrected the model twice on the same issue, /clear and restart with a sharper prompt; a clean session almost always beats a long, cluttered one.
Use /new inside the TUI to start a fresh thread (Codex uses /new, not /clear). A new thread resets conversational context but continues in the current checkout; create a separate git worktree when the new task also needs filesystem isolation.
Large-codebase AI workflows fail in specific, recognizable ways. Know the recovery for each.
CLAUDE.md/AGENTS.md carry the load