How to manage AI context windows in large monorepos
Strategies for preventing AI coding assistants from getting confused by massive monorepos. Covers workspace configuration, targeted indexing, and scoping queries.

How to manage AI context windows in large monorepos
The fastest way to make Cursor or GitHub Copilot useless in a large monorepo is to let it index everything. A repo with fifteen services, three frontend apps, and a shared packages directory will happily blow past any model's context window, and even when it technically fits, the assistant starts pulling in unrelated code, stale config, and dead branches that dilute every answer. The fix is not a bigger context window. It is scoping: restrict indexing to what matters, reference files explicitly, and treat cross-package changes as a manual verification step, not something the AI will catch for you.
Why monorepos break AI assistants differently than single repos
A single-service repo has a natural boundary. The assistant indexes it, and almost everything it finds is relevant to whatever you're working on. A Turborepo or Nx monorepo has no such boundary by default. You've got apps/web, apps/mobile, packages/ui, packages/api-client, services/billing, services/notifications, and a tools/ directory full of scripts nobody has touched in a year. All of it lives under one .git directory, and by default most AI coding tools will try to index all of it.
This causes two distinct problems. First, retrieval quality drops. When you ask "how does authentication work here," a semantic search over 40,000 files returns snippets from three different auth implementations across three different services, and the model has no way to know which one you actually mean. Second, and more insidiously, the model starts making suggestions that borrow patterns from the wrong package. A typical case: Copilot suggests a Redis client import that only exists in services/billing's dependencies, inside a file in services/notifications that has never touched Redis. The suggestion looked plausible because the pattern existed somewhere in the codebase. It just wasn't the right somewhere.
Restricting indexing to the packages you actually work in
Cursor's indexing behavior is documented in its codebase indexing docs, and the practical takeaway for monorepo users is that you have direct control over what gets indexed through ignore files. If you're working primarily in apps/web and packages/ui, you don't need services/billing or services/notifications in the index at all for most of your day-to-day work.
A .cursorignore file at the repo root, patterned after .gitignore syntax, is the blunt but effective tool here:
1services/billing/**2services/notifications/**3tools/legacy-scripts/**4**/*.snap5**/dist/**6**/.turbo/**A practical pattern is to keep two versions of this file and swap them depending on the week's work: a "frontend" version that excludes everything under services/ and a "backend" version that excludes apps/. Swapping them takes ten seconds and re-indexing a scoped subset of a large monorepo is dramatically faster than a full reindex, often the difference between a coffee break and a lunch break depending on repo size.
VS Code's approach is structurally different and, for genuinely separate projects that happen to share a repo, often better. Multi-root workspaces let you open only the folders relevant to your current task as a .code-workspace file, rather than mounting the entire monorepo root. Copilot's context gathering in VS Code respects that workspace boundary, so if your workspace file only declares apps/web and packages/ui as roots, that's the universe Copilot reasons about.
1{2 "folders": [3 { "path": "apps/web" },4 { "path": "packages/ui" },5 { "path": "packages/api-client" }6 ]7}This is more setup work than a single ignore file, but it pays off if you regularly switch between two or three "problem domains" in the same repo. A common setup is separate workspace files for frontend.code-workspace, billing-service.code-workspace, and full-repo.code-workspace, with the full-repo file reserved for the rare occasions you need to trace something end to end.
Using @ mentions instead of trusting semantic search
Ignore files solve the indexing problem, but the sharper tool for day-to-day scoping is being deliberate about what you reference in a given prompt. Cursor's @ mentions let you pull in a specific file, folder, or symbol directly rather than relying on the assistant's semantic search to guess what's relevant. Copilot Chat in VS Code has the equivalent with #file and #folder references.
The habit that actually matters: when you're debugging something in packages/api-client, don't ask a bare question and hope the retrieval layer finds the right file among forty thousand candidates. Reference it explicitly.
1@packages/api-client/src/client.ts @packages/api-client/src/types.ts23The retry logic in client.ts isn't respecting the timeout config4from types.ts. Walk through why.This is slower to type than a vague question, but it removes an entire class of failure where the model answers confidently about the wrong file. Without explicit references, it is easy to lose twenty minutes debugging a "fix" Copilot suggested for a useAuth hook, only to find the model had pulled in a similarly named hook from a different app in the same repo and blended the two implementations. Explicit references make that mistake structurally hard to make.
The trade-off is real: heavy reliance on @ mentions means you need to already know roughly where the relevant code lives. For genuine exploration ("where in this codebase do we validate webhook signatures") you do need semantic search across a broader scope, and that's exactly when a scoped .cursorignore earns its keep, because you want that broad search confined to the packages that could plausibly contain the answer, not the entire monorepo.
Context pollution from unrelated services is subtle, not obvious
The failure mode people expect is the model returning garbage or an obvious wrong answer. The failure mode that actually costs time is more subtle: the model gives an answer that's 80 percent right and stitches in a small assumption from an unrelated part of the repo, and you don't notice until it's already in a pull request.
A typical example: in an Nx monorepo with several NestJS services, you ask an assistant to add input validation to an endpoint. It produces a class-validator decorator using a @IsEnum pattern lifted from a completely different service's user-roles enum, because that service happened to be closer in the index (more recently touched, more heavily referenced elsewhere) than the enum you actually needed. The code compiles. It just validates against the wrong set of values, and nothing about the output looks suspicious in the diff.
The mitigation is boring but works: keep your ignore file scoped tightly enough that competing patterns from other services simply aren't in the retrieval pool, and when accuracy really matters (auth, payments, anything with a compliance requirement), paste the actual relevant enum or type definition into the prompt directly rather than trusting retrieval to find it.
The cross-package dependency gotcha
This is the one that bites almost everyone eventually, and it's worth calling out on its own because it doesn't look like a context problem at first. When you change a shared package, say packages/api-client, that's consumed by three different apps in a Turborepo or Nx workspace, AI assistants generally do not trace that dependency graph unless you explicitly point them at it.
Nx and Turborepo both understand the dependency graph through their own tooling (nx graph, turbo run build --graph), but Cursor and Copilot don't automatically read that graph as part of their context gathering. If you ask an assistant to "update the fetchUser function to add a new required parameter," it will happily update the function in packages/api-client and stop there, even though apps/web, apps/admin, and apps/mobile all call fetchUser and will now fail to build.
A seemingly trivial type change is enough to trigger it: adding a required field to a shared User interface in packages/types. The assistant updates the interface and the one call site you showed it. It does not touch the other nine call sites across three apps, because none of them were in context and nothing prompted it to look. The build breaks in CI twenty minutes later, on a change that looked complete in the editor.
The workaround is to make the dependency explicit in your prompt rather than assuming the tool will discover it:
1@packages/types/src/user.ts23I'm adding a required `tenantId` field to the User interface.4Find every consumer of this interface across apps/web, apps/admin,5and apps/mobile, and list the call sites that will break. Then6update each one.Even with that prompt, verify with a real build or a type check rather than trusting the model's claim that it "found all the usages." tsc --noEmit or your monorepo's affected-project command (nx affected:build, turbo run build --filter=...[origin/main]) is the ground truth here, not the assistant's confidence.
What this looks like in a normal week
In practice, a workflow that holds up across Nx and Turborepo codebases looks like this: keep a lean .cursorignore scoped to two or three active packages, swap it when you switch problem domains, use @ mentions for anything where you already know the file, and reserve broad unscoped questions for genuine exploration where you're deliberately casting wide. Treat any shared package or type change as requiring a manual sweep, either through the monorepo's own affected-project tooling or a full type check, because no assistant reliably traces that graph on its own yet.
None of this is exotic. It's closer to good git hygiene than to prompt engineering. The teams that get the most out of Cursor or Copilot in a monorepo aren't the ones with clever prompts, they're the ones who treat context scope as a first-class setting they adjust as often as they switch branches.
Related Articles

How to configure .cursorignore to prevent context pollution and protect secrets
Cursor indexes your working directory, not just what Git tracks, so an untracked .env file can still leak straight into a prompt. Here's how to lock that down properly.

How to manage API costs when using Claude Sonnet for automated coding
Explains how to utilize Anthropic's Prompt Caching to reduce API bills when using tools like Cline or Continue.dev. Details the mechanics of context window reloading and how to structure files to maximize cache hits.

How to fix Cursor codebase indexing issues on large repositories
Reinstalling Cursor rarely fixes a broken index. On monorepos above roughly 50,000 files, an aggressive .cursorignore plus a manual reindex almost always does instead.