# Large codebase strategies in Cursor

URL: https://developertoolkit.ai/en/cursor-ide/build/large-codebase-strategies/
Description: Keep Cursor's agent accurate in a large repo or monorepo: open one package, filter noise, scope rules, plan cross-package changes, and verify with gates.

A large codebase strategy in Cursor narrows what the agent sees to the slice a task touches: open the package instead of the monorepo root, keep tracked noise out with `.cursorignore`, load conventions through glob-scoped rules, and split cross-package work into planned steps. Each step ends with a scope check, a leftover-pattern count, and tests on dependent packages.

This page is for developers and tech leads working in a repository past roughly 100,000 lines, usually a TypeScript monorepo. You ask the agent for a notification-preferences endpoint and it writes a new service from scratch instead of using the one three directories down. It copies a pattern from `packages/legacy-billing/`, which nobody has touched in two years. Halfway through a rename it forgets which packages it already finished. None of this is a Cursor defect. The agent reads a slice of the repository, and in a large repository the default slice is the wrong one.

**How this page was checked**

Runtime behaviour on this page (which ignore files the agent honours, where rules load from, the `.mdc` format) was checked on 2026-09-26 against the agent runtime shipped in Cursor's `@cursor/sdk` 1.0.32 (npm, 2026-09-22). On 2026-10-12 Cursor's docs confirmed the `.mdc` format and the AGENTS.md and CLAUDE.md behaviour described in [Write down what the code cannot show in scoped rules](#write-down-what-the-code-cannot-show-in-scoped-rules). UI labels and menu paths are omitted; confirm them in your build.

## What you get from scoping Cursor to a large codebase

- A way to open a large repository so the agent's search starts in the right package.
- A `.cursorignore` aimed at the files `.gitignore` does not already cover.
- A root rule, glob-scoped package rules and module summaries that tell the agent what the code cannot.
- Four copy-paste prompts: map an area, plan a cross-package change, execute one step, and audit stale rules.
- A `verify-change.sh` gate that proves scope, completeness and consumer health without a line-by-line review.

## Why does Cursor's agent get worse as the repository grows?

The agent answers from what it reads, and it reads a fraction of the repository. In a large codebase three things go wrong, and each has its own fix.

| Symptom | Cause | Fix on this page |
| --- | --- | --- |
| Copies a deprecated module, cites generated code | Search finds the wrong files first | [Open the package](#open-the-package-you-work-in-not-the-monorepo-root) and [filter tracked noise](#keep-tracked-noise-out-of-the-agents-search-with-cursorignore) |
| Writes a new helper instead of using yours | Conventions live in people's heads, not in the code | [Scoped rules and module summaries](#write-down-what-the-code-cannot-show-in-scoped-rules) |
| Forgets earlier decisions, repeats work | One chat carries too much history | [Planned steps, one chat each](#make-a-cross-package-change-in-planned-steps) |

A larger context window does not remove any of these causes. More room lets the agent hold more of the wrong files. Which files to attach and when to let the agent search is covered in [context management in Cursor](/en/cursor-ide/quick-start/context-management/); this page applies those habits to repositories that do not fit in any context window.

## Open the package you work in, not the monorepo root

Where you open Cursor decides where the agent's search starts and which rules load. The rule loader in `@cursor/sdk` 1.0.32 always reads `.cursor/rules/` in the opened directory and in every directory above it. Opening `packages/api/` therefore loads the API package's own rules and the repository's root rules, while the search starts among API files.

1. For work inside one package, open that package's directory as the folder.

2. For work that spans two or three packages, create a multi-root workspace file in the repository root and open it instead of the root folder. Cursor, as a VS Code fork, opens VS Code multi-root `.code-workspace` files; confirm this in your build:

   ```json
   // api-web.code-workspace
   {
     "folders": [
       { "path": "packages/api", "name": "api" },
       { "path": "packages/web", "name": "web" },
       { "path": "packages/shared", "name": "shared" }
     ]
   }
   ```

3. Commit the workspace files your team reuses, one per common combination, and name them after the task (`api-web.code-workspace`, not `workspace2.code-workspace`).

4. Open the whole root only for tasks that are cross-cutting by nature, such as a repository-wide rename, and pair them with the plan in [Make a cross-package change in planned steps](#make-a-cross-package-change-in-planned-steps).

Across separate repositories, rather than packages in one, the same idea extends to contract rules and a contract-first workflow; see [multi-repo workflows in Cursor](/en/cursor-ide/build/multi-repo-workflows/).

## Keep tracked noise out of the agent's search with `.cursorignore`

Cursor's agent reads both `.gitignore` and `.cursorignore` when it walks the workspace, and both use the same syntax (checked in `@cursor/sdk` 1.0.32). Anything already in `.gitignore`, such as `node_modules/` or `dist/`, is excluded without a second entry. `.cursorignore` is for files that are **committed** but that the agent must never copy from:

```gitignore
# .cursorignore: committed files the agent must not read or imitate

# Generated code that is checked in
packages/api/src/generated/
packages/web/src/__generated__/
*.generated.ts

# Deprecated modules kept only until the migration finishes
packages/legacy-billing/

# Vendored third-party code and large fixtures
vendor/
packages/api/test/fixtures/large/

# Lockfiles and minified bundles
pnpm-lock.yaml
*.min.js
```

Two rules keep the file useful. Never exclude live source, tests or configuration, because the agent needs them to match your patterns. And treat the deprecated-module entry as temporary: delete it when the module is gone, or the agent cannot help with the migration that removes it. A path blocked this way fails with an error that names `.cursorignore` when the agent tries to read it, which also makes a wrong entry easy to spot.

To measure what an ignore change does to indexing and search speed, with before-and-after numbers, see [tuning Cursor for large codebases](/en/cursor-ide/operate/performance-optimization/). For the security use of the same file, see [privacy and security for Cursor teams](/en/cursor-ide/team/privacy-security/).

## Write down what the code cannot show in scoped rules

A new hire needs a map and a few house rules before touching a 300,000-line repository, and so does the agent. Project rules are `.mdc` files in `.cursor/rules/`. A plain `.md` file there is skipped by the rule loader (checked in `@cursor/sdk` 1.0.32), so it fails silently; Cursor's docs say the same (checked 2026-10-12). Three layers cover a large repository.

**1. One short always-on map at the root.** Keep it to what every task needs: where things live, what is deprecated, and the gate commands.

```markdown
---
description: Repository map and gates for the acme monorepo
alwaysApply: true
---

- pnpm monorepo. Live packages: packages/api (Fastify), packages/web (Next.js),
  packages/shared (types only, no runtime code).
- packages/legacy-billing is deprecated. Never import from it or copy its patterns;
  the replacement is packages/api/src/billing.
- Before reporting a task done, run for each package you touched:
  pnpm --filter <package> run typecheck && pnpm --filter <package> test
```

**2. Glob-scoped rules per package.** A rule with `globs` loads when a matching file is in context, so API conventions never reach web work. Put it in the package it governs, for example `packages/api/.cursor/rules/api-conventions.mdc`, and write the glob from the package root, the folder you open. Confirm it matches with the [probe routine in custom rules and templates](/en/cursor-ide/automate/custom-rules-templates/):

```markdown
---
description: HTTP handler conventions for packages/api
globs: src/**/*.ts
alwaysApply: false
---

- Handlers call services; services call repositories. No SQL in handlers.
- Validate input with the Zod schemas in packages/api/src/schemas/.
- Errors use AppError from packages/api/src/errors.ts, never a bare throw.
- Canonical example: packages/api/src/routes/users/index.ts
```

**3. A summary file for each large module.** For a module too big to attach, a one-page `ARCHITECTURE.md` gives the agent the map for a fraction of the tokens. Reference it from the module's rule so the agent reads it before opening individual files:

```markdown
<!-- packages/api/src/payments/ARCHITECTURE.md -->
# Payments
- service.ts: checkout and refunds (Stripe). repository.ts: Drizzle queries.
- webhook.ts: verifies the signature, then updates status via repository.ts.
- Invariants: amounts are integer cents; every Stripe call sends an idempotency key.
```

When you open the monorepo root instead of the package, the loader reaches rules nested in `packages/*/.cursor/rules/` only when nested-rule discovery is on, which it is by default in `@cursor/sdk` 1.0.32, and a glob written from the package root may not match from the repository root. Prove which rules load with the probe routine in the rules guide rather than assuming. Load modes, templates and a CI check that fails when a glob matches nothing are covered in [custom rules and templates](/en/cursor-ide/automate/custom-rules-templates/). If you are writing your first rule, start with [project rules](/en/cursor-ide/quick-start/project-rules/).

**AGENTS.md and CLAUDE.md in a monorepo.** Cursor's docs (checked 2026-10-12) say Cursor supports AGENTS.md in the project root and subdirectories. Nested files are combined with those in parent directories, and more specific instructions take precedence, so an `AGENTS.md` in `packages/api/` adds to the one at the repository root and wins where the two differ. Cursor's help says Cursor reads `CLAUDE.md` files the same way it reads AGENTS.md, and always applies them to every conversation, regardless of any `alwaysApply` frontmatter setting (checked 2026-10-12). In Cursor 3.21 (2026-09-17), team rules with file patterns apply only to the files that match.

## Map an unfamiliar area before you change it

Before the first edit in code you did not write, ask for a read-only map with file paths. The map becomes the context for the next chat, and its file list becomes the scope you check the change against.

**Copy-paste prompt: map an area before changing it**

```text
Read-only task: do not edit any file.

Map how POST /api/orders is handled in packages/api. Start at the route
registration and follow the call chain through middleware, handler,
service, repository and any queue or webhook it triggers.

Reply with:
1. A numbered call chain, one line per step: file path, function, what it does.
2. Error paths: what happens on validation failure, database error, and
   payment provider rejection, with the file that handles each.
3. Anything resolved indirectly (string-registered routes, dependency
   injection, event names) that a text search would miss.
4. Files under packages/legacy-billing/ that are still on the live path.
   Say "none" if there are none.
```

Check the answer against the code with two or three spot checks: open the files it names at the lines it names. A map with a wrong file path is a sign the agent guessed, and the fix is a narrower question, not a longer one.

## Make a cross-package change in planned steps

A change that crosses packages fails when one chat tries to do it all. Plan it once, write the plan to a file, and execute one package per chat with a gate after each step.

1. Switch the agent to Plan Mode, which creates an implementation plan before writing any code, and ask for a plan file. Review the plan, not code: the caller list and the step order are where mistakes are cheap to fix.

   **Copy-paste prompt: plan a cross-package change**

   ```text
   Plan, do not implement. Goal: add organizationId (string, required) to
   the User type and carry it through the API and the web user form.

   Write the plan to docs/plans/user-organization-id.md with:
   - Every file to change, grouped by package, in dependency order:
     packages/shared first, then packages/api, then packages/web.
   - Every caller of the User type outside those files, found with a
     text search and the language server, and whether it needs a change.
   - The database migration, and how existing rows get a value.
   - For each step: the command that proves it, using
     pnpm --filter <package> run typecheck and pnpm --filter <package> test.
   - Files you will not touch, including anything in packages/legacy-billing/.
   ```

2. Execute one step per chat. A new chat per package keeps earlier attempts out of the context, and the plan file carries the decisions forward.

   **Copy-paste prompt: execute one planned step**

   ```text
   Read @docs/plans/user-organization-id.md. Execute only the first step
   that is not marked done.

   Change only the files that step lists. If you need to change any other
   file, stop and tell me which file and why.

   When the edits are finished, run the step's gate commands and show their
   output. If a gate fails, fix it within the same files. Then mark the step
   done in the plan file with the commit-ready summary: files changed and
   the gate result.
   ```

3. Commit after each green step. A small commit is the unit you can revert, and the next chat starts from a known-good tree.

4. For long migrations that span many packages, run independent steps in parallel in separate worktrees, or hand them to Cloud Agents; see [Cloud Agents and automations](/en/cursor-ide/automate/cloud-agents-automations/).

Whether the agent runs gate commands without asking depends on your run mode. Configure run modes and the sandbox as described in [privacy and security for Cursor teams](/en/cursor-ide/team/privacy-security/) rather than approving each command by hand.

## How do you verify a large-codebase change without reading every diff?

Prove three things mechanically, then spend human attention on the plan and on whatever the checks flag.

| Question | Check | Passes when |
| --- | --- | --- |
| Did the change stay in scope? | `git diff --name-only` compared with the plan's file list | Every changed file is in the plan, or has a stated reason |
| Is it complete? | An `rg` count of the old pattern | The count is 0 |
| Does every consumer still work? | Typecheck and tests on changed packages **and their dependents** | Green on all of them |

```bash
#!/usr/bin/env bash
# verify-change.sh: run in a terminal at the repository root after the last step
set -euo pipefail
base=origin/main

echo "== Files changed (compare with docs/plans/user-organization-id.md)"
git diff --name-only "$base"...HEAD

echo "== Files changed outside the allowed packages (must be empty)"
outside=$(git diff --name-only "$base"...HEAD \
  | grep -v -E '^(packages/(shared|api|web)/|docs/plans/)' || true)
echo "$outside"
test -z "$outside"

echo "== Imports from the deprecated module (must be 0)"
count=$( { rg -n "from ['\"].*legacy-billing" packages/ --type ts --glob '!packages/legacy-billing/**' || true; } | wc -l)
echo "$count"
test "$count" -eq 0

echo "== Typecheck and test every changed package and its dependents"
pnpm --filter "...[$base]" run typecheck
pnpm --filter "...[$base]" test
```

The `...[origin/main]` filter selects packages changed since `origin/main` plus every package that depends on them, which is where a shared-type change breaks. Nx (`nx affected -t typecheck test`) and Turborepo (`turbo run typecheck test --filter=...[origin/main]`) do the same for their repositories. Swap the `rg` pattern for whatever the change removes: an old function name, a positional call style, a deprecated import.

Run the same script in CI so the gate does not depend on anyone remembering it. Then let [Bugbot](/en/cursor-ide/ship/bugbot/) review the pull request for correctness, and route it through `CODEOWNERS` so each package owner signs off on their slice. The owner reads the plan and their package's gate output, not every line. The wider review setup is in [code review with Cursor](/en/cursor-ide/ship/code-review/).

## Keep long sessions accurate in a large repository

- **One task per chat.** Start a new chat when a step is committed, when you move to another package, or when the agent repeats a mistake you already corrected. Carry state in the plan file or a short handoff note, as shown in [context management in Cursor](/en/cursor-ide/quick-start/context-management/).
- **Attach one canonical example, not a folder.** `@packages/api/src/routes/users/index.ts` costs a few thousand tokens; mentioning all of `packages/api/src/` can fill the window before the agent starts. [Token management in Cursor](/en/cursor-ide/operate/token-management/) shows how to measure the difference.
- **Keep rules in step with the code.** A rule that describes last year's architecture misleads the agent in every chat it loads into. Audit them after large refactors.

**Copy-paste prompt: find rules that no longer match the code**

```text
Read-only task: do not edit any file.

For each .mdc file under .cursor/rules/ and packages/*/.cursor/rules/:
1. List every file path, function or package it names, and whether that
   still exists.
2. Quote each instruction that the current code contradicts, and name the
   file that contradicts it.
3. Flag any glob that matches no file in the repository.
Propose a corrected line for each problem. Output a table.
```

## What breaks in a large codebase in Cursor, and how to recover

### The agent copies a deprecated or generated module

Search found the old module first. Recovery: restore the checkpoint from before the edit (see [checkpoints and branching](/en/cursor-ide/build/checkpoints-branching/)), add the path to `.cursorignore` or name it as deprecated in the root rule, and rerun the step in a new chat.

### The agent writes a new helper instead of using the existing one

The convention exists only in people's heads. Recovery: add the canonical file to the package's glob-scoped rule, then rerun with that file attached. If it happens twice for the same helper, add a lint rule that bans the duplicate; lint fails in CI, a rule only asks.

### The agent edits a similarly named file in another package

`packages/web/src/user.ts` and `packages/api/src/user.ts` look the same to a search. Recovery: open the package or its workspace instead of the root, write full paths in the prompt, and let the scope check in `verify-change.sh` catch the stray file.

### Indexing never finishes, or search is slow

A large generated or binary file sits in a tracked directory. Recovery: find it with `git ls-files -z | xargs -0 du -k | sort -rn | head -20`, add it to `.cursorignore`, and follow the measurement steps in [tuning Cursor for large codebases](/en/cursor-ide/operate/performance-optimization/).

### A package's rule never loads

The file is saved as `.md` instead of `.mdc`, or its glob matches nothing. Recovery: rename the file, test the glob against `git ls-files`, and add the CI check from [custom rules and templates](/en/cursor-ide/automate/custom-rules-templates/).

### The agent loses the thread halfway through a change

The chat carries too many attempts. Recovery: stop, make sure the plan file marks what is done, commit what passes its gate, and continue from the first unfinished step in a new chat.

## How Claude Code and Codex handle large repositories

The scoping principles are the same across tools; the configuration is not. Claude Code scopes with the start directory, nested `CLAUDE.md` files and deny rules, covered in [large codebases and monorepos in Claude Code](/en/claude-code/build/large-codebase/). Codex keeps long sessions on track with `AGENTS.md` files and handoff files, covered in [context management across Codex surfaces](/en/codex/build/context-patterns/). For the cross-tool strategy, see [strategies for million-line codebases](/en/workflows/build/large-codebases/) and [monorepo workflows with AI assistants](/en/workflows/build/monorepo-management/).

## Where to go next with large codebases in Cursor

- Before this page: [context management in Cursor](/en/cursor-ide/quick-start/context-management/) for the attach-or-search habits this page builds on.
- Next: [refactoring strategies in Cursor](/en/cursor-ide/build/refactoring-strategies/) for renames and restructuring one verified step at a time, and [legacy code refactoring](/en/cursor-ide/build/legacy-refactor/) for code without tests.
- Across repositories: [multi-repo workflows in Cursor](/en/cursor-ide/build/multi-repo-workflows/).
- To choose a model for long, cross-package work: [model selection in Cursor](/en/cursor-ide/quick-start/model-selection/).
