Large codebases and monorepos in Claude Code: scope, plan, verify
PLClaude Code works in a large codebase or monorepo when each session sees only the slice a task touches. The launch directory, per-directory CLAUDE.md files, claudeMdExcludes, Read deny rules, a code intelligence plugin, and sparse worktrees control that slice. Cross-package changes stay safe when they are planned in one session and checked by gates on every affected package.
This page is for developers working in a repository too big to hold in their head, and for tech leads who own its shared Claude Code configuration. The situation: you ask Claude to change the auth flow in a 40-package monorepo, and it edits the deprecated legacy-auth package instead of the live one. Two weeks later a teammate’s session reads the frontend team’s CLAUDE.md and applies React rules to a Go service. Neither is a model problem. Both are scoping problems.
Everything below was checked against Claude Code v2.1.283 (the latest channel) and the vendor’s large codebases guide on 2026-09-26.
What you get from scoping Claude Code to a large codebase
Section titled “What you get from scoping Claude Code to a large codebase”- A rule for where to start
claudeso the rightCLAUDE.mdfiles and settings load. - A committed settings file that keeps other teams’ instructions, build output, and vendored code out of context.
- Prompts for a complete caller inventory, a cross-package plan, and a gated execution run.
- A worktree setup that lets parallel sessions work without overwriting each other.
- A verification script that proves scope, completeness, and behaviour on every affected package, so nobody has to read every line.
- Recovery steps for the failures that only show up at scale.
Where should you start Claude in a large repository?
Section titled “Where should you start Claude in a large repository?”The directory you launch claude from decides what Claude can edit without asking, which CLAUDE.md files load at startup, and which .claude/settings.json applies. Project settings are not inherited from parent directories the way CLAUDE.md files are.
| Start from | File access | CLAUDE.md loaded at launch | Use when |
|---|---|---|---|
| Repository root | Every file | Root only; a subdirectory’s file loads when Claude reads a file there | The task spans several packages or subsystems |
| A package or subsystem | That subtree, until you grant more | That directory’s file and every ancestor’s | The work belongs to one package |
Default to the package. Start from the root only when the change genuinely crosses packages, and then plan it first (see plan a cross-package change).
To confirm what loaded, run /context in the session and read the Memory files list. If a file from another team’s package appears there, the next section removes it.
Scope what Claude loads and reads
Section titled “Scope what Claude loads and reads”Each setting below is independent, so adopt the ones your repository needs. The tech lead commits the shared parts; each developer keeps personal exclusions in .claude/settings.local.json.
-
Split
CLAUDE.mdby directory. Keep the root file to rules that apply everywhere (commit format, where generated code lives, how to run package scripts). Put each package’s stack conventions in that package’sCLAUDE.md, maintained by its owners. For the full loading order, imports and.claude/rules/withpaths:globs, see the Claude Code memory system.packages/api/CLAUDE.md Copy `.env.example` to `.env` before running tests; they fail without it.Write queries with the Knex query builder. No raw SQL in route handlers.Never edit a merged migration. Add a new one.Gate: `pnpm --filter api run typecheck && pnpm --filter api test`. -
Exclude instructions for code you never touch. When you start from the root, a package’s
CLAUDE.mdloads as soon as Claude reads a file in it.claudeMdExcludesskips files by glob, matched against absolute paths, so start relative patterns with**/. Put personal exclusions in.claude/settings.local.jsonand add that file to.gitignoreif you created it by hand. Managed-policyCLAUDE.mdfiles cannot be excluded.{"claudeMdExcludes": ["**/packages/legacy-*/**", "**/packages/web/CLAUDE.md"]} -
Deny reads of generated and vendored code. Content search already respects
.gitignore, sonode_modules/anddist/stay out of results. For checked-in generated code or a vendored SDK, addReaddeny rules. Ending a pattern with/**/*blocks the files but still lets Claude list the directory. Deny rules cover Claude’s file tools and the Bash file commands Claude Code recognises (cat,grep,findwith the path as an argument), but a recursivegrep -rover a parent directory still returns matches from denied files, and subprocesses that open files themselves are not covered.{"permissions": {"deny": ["Read(./**/generated/**/*)","Read(./**/*.generated.*)","Read(./**/vendor/**/*)","Read(./packages/legacy-auth/**/*)"]}}Relative patterns anchor at the directory you start
claudefrom, so the last rule protects sessions started at the repository root. If you start sessions from a package, write repo-wide rules as//-absolute paths, for exampleRead(//abs/path/to/repo/packages/legacy-auth/**/*). -
Replace file scans with a language server. A code intelligence plugin lets Claude jump to definitions and find references instead of reading files to locate a symbol, and it reports type errors after edits. Install the language server binary first, then the plugin, inside a session. For TypeScript:
Terminal window # terminalnpm install -g typescript-language-server typescript/plugin install typescript-lsp@claude-plugins-officialThe official marketplace also carries
pyright-lsp,gopls-lsp,rust-analyzer-lsp,jdtls-lsp, and other language plugins (listed in anthropics/claude-plugins-official on 2026-09-26). To enable a plugin for the whole team, add it toenabledPluginsin the committed.claude/settings.json. If your organization already runs a code search or RAG index over the repository, expose it as an MCP server so Claude queries the index instead of reading files; see code intelligence MCP servers. -
Grant sibling access explicitly. A session started in
packages/api/cannot editpackages/shared/until you allow it. Commit the siblings every API developer needs, or pass--add-dirat launch (or run/add-dirin a session) for a one-off:{"permissions": { "additionalDirectories": ["../shared"] }}Terminal window # terminal, from packages/apiCLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 claude --add-dir ../sharedA directory added through
additionalDirectoriesnever loads itsCLAUDE.md, rules or skills. One added with--add-dirloads its skills, and loads itsCLAUDE.mdand rules only when that environment variable is set.
Package-specific procedures that are too long for CLAUDE.md, such as how to write API tests, belong in a skill under packages/api/.claude/skills/. Skills load only when Claude judges them relevant, so API tooling costs nothing during frontend work.
Explore a large area without filling the main context
Section titled “Explore a large area without filling the main context”Before you change anything that crosses packages, get an inventory. Run the search in a subagent so hundreds of file reads stay out of your main conversation, and name the indirection your codebase uses, because text search finds imports but not handlers registered by string key or dependency injection.
Run the rg command it gives you and compare the count with the table. A mismatch means the inventory is incomplete; ask Claude to explain each missing hit before you plan. For a full onboarding map of an unfamiliar repository, see understanding an unfamiliar codebase with Claude Code.
Make a cross-package change in one planned session
Section titled “Make a cross-package change in one planned session”A change to a shared type and all its call sites belongs in one session, so every edit follows the same decisions. Plan it first: in plan mode Claude writes the plan to a file, and Claude Code re-injects that plan after each compaction, so a long session keeps its plan when older conversation history is summarised.
Read the plan, not the code. Check the edit order, the uncovered call sites and the “files outside this list” line. Add tests for the uncovered call sites before approving, or accept the gap in writing.
If the change splits into independent units that can each merge on its own, such as the same migration in 30 packages, hand it to /batch instead; see batch operations across hundreds of files.
Run parallel sessions without collisions
Section titled “Run parallel sessions without collisions”Two sessions in the same checkout share one working tree, so the second can overwrite the first. Give each parallel session its own git worktree:
# terminal, one per taskcd packages/api && claude --worktree api-rate-limitcd packages/web && claude --worktree web-settings-pageIn a large repository a full worktree is slow to create and heavy on disk. Commit worktree.sparsePaths so each worktree checks out only the listed directories plus root-level files, and symlinkDirectories so node_modules is linked instead of copied. Paths are relative to the repository root. Include .claude so the root settings and rules exist inside the worktree. Put sparsePaths and symlinkDirectories in the .claude/settings.json of the directory you launch from (here packages/api/); they are read before the worktree exists.
{ "worktree": { "sparsePaths": [".claude", "packages/api", "packages/shared"], "symlinkDirectories": ["node_modules"] }}Two rules keep parallel work mergeable. First, one owner per file: if two tasks must both change packages/shared, sequence them instead of running them side by side. Second, inside a worktree the session reads the worktree root’s .claude/settings.json, not the package’s, so keep a copy of your Read deny rules in the repository root’s settings. For supervising many sessions at once, see running many agents in parallel with worktrees.
How do you verify a large-codebase change without reading every diff?
Section titled “How do you verify a large-codebase change without reading every diff?”Prove three things mechanically, then spend human attention only on the plan and on the files the checks flag.
| Question | Check | Passes when |
|---|---|---|
| Did the change stay in scope? | git diff --name-only against 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, not only the package you edited |
#!/usr/bin/env bash# verify-change.sh: run from the repository root after the session finishesset -euo pipefailbase=origin/main
echo "== Files changed (compare with the plan)"git diff --name-only "$base"...HEAD
echo "== Positional authenticate( calls left (must be 0)"# rg exits 1 when nothing matches, which is the passing case, so || true keeps set -e and pipefail from abortingcount=$( { rg -nP 'authenticate\((?!\s*\{)' packages/ --type ts --glob '!packages/legacy-auth/**' || true; } | wc -l)echo "$count"test "$count" -eq 0
echo "== Typecheck and test every changed package and its dependents"pnpm --filter "...[$base]" run typecheckpnpm --filter "...[$base]" testAdjust the pattern to your call style: exclude the file that defines UserService (for example --glob '!packages/shared/src/user-service.ts') and any unrelated authenticate( APIs. The language server’s reference list is the tie-breaker.
The ...[origin/main] filter selects packages changed since origin/main plus every package that depends on them, which is where a signature change breaks. Nx (nx affected -t typecheck test) and Turborepo (turbo run typecheck test --filter=...[origin/main]) have the same idea. Run the same script in CI so the gate does not depend on anyone remembering it.
Then run /code-review on the branch for a second pass focused on correctness, and route the pull request through CODEOWNERS so the owner of each touched package signs off on their slice. The owner reviews the plan and the gate output for their package, not every line. For the review and CI side, see review automation for Claude Code.
What breaks in a large codebase, and how do you recover?
Section titled “What breaks in a large codebase, and how do you recover?”Claude edits the wrong package
Section titled “Claude edits the wrong package”A deprecated package with the same names as the live one (legacy-auth next to auth) wins the search. Recovery: /rewind the edits, add the deprecated path to Read deny rules and claudeMdExcludes, and restart from the live package’s directory. Add one line to the root CLAUDE.md naming the live package.
The caller inventory misses indirect references
Section titled “The caller inventory misses indirect references”Text search finds imports, not routes registered by string or services resolved through a container. The change compiles and fails at runtime. Recovery: name each indirection mechanism in the prompt, cross-check with rg, rely on the language server’s references where you have one, and add a test for every call site the plan marked as uncovered.
The context fills halfway through a cross-package change
Section titled “The context fills halfway through a cross-package change”Claude starts repeating work or forgets an earlier decision. Recovery: run /compact with a focus, for example /compact keep the plan, the caller table, and the list of packages already done. The plan file is re-injected after compaction. If quality is still poor, /clear and start a new session that reads the plan and continues from the first unfinished package.
A package’s conventions are ignored after --add-dir
Section titled “A package’s conventions are ignored after --add-dir”You added ../shared and Claude ignored its CLAUDE.md. Directories added with --add-dir load their CLAUDE.md only when CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 is set, and additionalDirectories never loads it. Recovery: restart with the variable set, or start from the root for this task.
Deny rules stop working inside a worktree
Section titled “Deny rules stop working inside a worktree”A worktree session reads the root .claude/settings.json, so rules committed only in packages/api/.claude/settings.json do not apply there. Recovery: copy the deny rules into the root settings file.
Per-package skills stop triggering
Section titled “Per-package skills stop triggering”A long session started from the root can accumulate hundreds of skills from every directory it touches. When there are many, some skills lose their descriptions entirely, so Claude no longer has the keywords it needs to pick the right one. Recovery: start from the package, keep descriptions short and lead with the words a request contains (“writing or modifying tests in packages/api/”), and move shared skills into a plugin.
Instructions drift from the code
Section titled “Instructions drift from the code”A CLAUDE.md still describes session-based auth after the move to JWT. Recovery: review CLAUDE.md changes in pull requests like code, run the reconcile prompt above after large refactors, and re-read your instructions after a major model release, since workarounds for an older model become overhead.
How Codex and Cursor handle large repositories
Section titled “How Codex and Cursor handle large repositories”The scoping principles are the same across tools, but the configuration is not: Codex reads AGENTS.md files and Cursor uses its own rules. For the cross-tool strategy, see strategies for million-line codebases and monorepo workflows with AI assistants. For Cursor’s version of this page, see Cursor large codebase strategies.
Where to go next with large codebases
Section titled “Where to go next with large codebases”- Before this page: understanding an unfamiliar codebase with Claude Code builds the map you scope against.
- Next: multi-file workflows for coordinated edits inside one package, and large-scale refactoring from the terminal for renames and pattern migrations.
- For token spend and context size on big repositories: performance optimization for Claude Code. Context-window sizes per model are on the models hub.
- To run per-package linters after each edit automatically: hooks automation.