Claude Code works on a codebase of any size; in a large one, the job is to keep its context focused: split instructions into per-directory CLAUDE.md files, exclude the ones you never need, block reads of generated and vendored code, and start Claude in the directory you are working on. Subagents and plan mode then keep long tasks on track.
Anthropic's official page on monorepos and large codebases puts the problem this way: "as the codebase grows, the defaults tuned for smaller projects can fill the context window with instructions and file reads unrelated to the task, costing tokens and degrading Claude's performance." This guide walks through each fix and notes whether the setting is personal or committed.
Where should you start Claude in a large repo?
Your launch directory decides three things: which files Claude can edit without extra permission, which CLAUDE.md files load at startup, and which project settings apply.
| Start from | File access | CLAUDE.md at launch | Use when |
|---|---|---|---|
| Repository root | Every file | Root only; subdirectories load on demand | Tasks span several packages |
| A subdirectory | That subtree, until you grant more | That directory's plus every ancestor's | Work is scoped to one package |
One catch: project settings in .claude/settings.json are not inherited from parent directories the way CLAUDE.md files are. If you start from packages/api/, that package needs its own .claude/settings.json.
How should you layer CLAUDE.md files?
A single root CLAUDE.md in a big repo either grows to cover every subsystem or stays too generic to help. The docs recommend two levels:
- Root
CLAUDE.md: rules that apply everywhere, such as coding standards and commit conventions. - Per-directory
CLAUDE.md: conventions for that area's stack, one per package in a monorepo or one per subsystem likesrc/db/in a single tree.
Claude Code loads every CLAUDE.md from your working directory and its parents at launch, then loads a subdirectory's file once Claude reads, writes or edits a file there. Files are concatenated, root first, so instructions closer to where you launched are read last. The Claude Code memory guide explains loading order in full.
Commit these files and let each directory's owner maintain theirs. The docs suggest reviewing CLAUDE.md edits in pull requests, revisiting them after major model releases, and optionally adding a Stop hook that reviews the session transcript and proposes updates.
If your repo uses AGENTS.md instead, a nested AGENTS.md also loads on demand, when Claude reads a file in that directory. See Claude Code and AGENTS.md.
When should you use path-scoped rules instead?
Rules in .claude/rules/ are the other way to target instructions. A rule with paths frontmatter loads only when Claude works with a matching file:
---
paths:
- "src/api/**/*.ts"
---
- All API endpoints must include input validation
- Use the standard error response format
The docs frame the choice like this:
| Approach | Lives | Loads when | Best for |
|---|---|---|---|
Per-directory CLAUDE.md |
Next to the code | At launch from that directory, or on demand | Owners maintaining their own conventions |
| Path-scoped rule | Central .claude/rules/ |
Claude touches a file matching paths |
One central place, or rules for scattered paths |
Rules without paths load at launch like .claude/CLAUDE.md, so keep those short.
How do you exclude irrelevant CLAUDE.md files?
From the repository root, other teams' files can load on demand during a session. The claudeMdExcludes setting skips files by path or glob, matched against absolute paths, so start patterns with **/:
{
"claudeMdExcludes": [
"**/packages/web/**"
]
}
That skips every CLAUDE.md and rules file under the package. Put it in .claude/settings.local.json for yourself, or in project settings for the team; arrays merge across scopes. A managed CLAUDE.md from your organization cannot be excluded. The list is static, so to focus on a different package each day, start Claude from that package instead of editing exclusions.
How do you reduce what Claude reads?
Instructions are one cost; file reads are the other. Claude's content searches respect .gitignore, so node_modules/, dist/ and similar paths already stay out of results. For checked-in paths such as a vendored SDK or committed generated code, add Read deny rules:
{
"permissions": {
"deny": [
"Read(./**/dist/**/*)",
"Read(./**/build/**/*)",
"Read(./**/*.generated.*)",
"Read(./**/vendor/**/*)"
]
}
}
Ending patterns with /**/* blocks the contents but lets Claude still list or cd into the directory. Deny rules cover the built-in file tools and recognized Bash commands such as cat and grep with a denied path as an argument, but a recursive grep -r over a parent directory still shows denied files, and subprocesses that open files themselves are not covered.
Code intelligence plugins cut reads further. They connect Claude to a language server so it can jump to definitions and find references instead of scanning files. The official marketplace has plugins for TypeScript, Python, Go, Rust and other languages, for example /plugin install typescript-lsp@claude-plugins-official. Each developer needs the language server binary installed. See Claude Code plugins for installing and sharing them.
How do worktrees and extra directories work at scale?
claude --worktree checks out the whole repository by default. In a large repo, worktree.sparsePaths uses git sparse-checkout to write only the listed directories plus root-level files:
{
"worktree": {
"sparsePaths": [".claude", "packages/api", "packages/shared"],
"symlinkDirectories": ["node_modules"]
}
}
Include .claude if you want root settings and rules inside the worktree, and symlinkDirectories avoids copying node_modules into every worktree. All worktrees in a session, including subagent worktrees, share the same sparsePaths. More in the worktrees guide.
When you start from one package and need a sibling, grant access with permissions.additionalDirectories in settings or --add-dir at launch. The difference matters: additionalDirectories grants file access only, while --add-dir also loads that directory's skills, and loads its CLAUDE.md and rules if you set CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1.
How do subagents and skills help in a big repo?
Research is what fills context fastest. The best practices page advises delegating it: "use subagents to investigate X". Subagents run in their own context windows and report back summaries, so the hundreds of file reads stay out of your main conversation. The built-in Explore subagent is read-only and is what Claude uses for codebase search; see the subagents guide.
Skills are the on-demand counterpart to CLAUDE.md. Put package-specific procedures in packages/api/.claude/skills/, or scope a root skill with paths frontmatter. With many skills, keep descriptions short and lead with words a request would contain, because when the list is long some skills lose their descriptions. Shared skills that need versioning across repos can ship as a plugin.
For cross-package changes, the docs recommend giving Claude the whole change in one session and planning first in plan mode. Claude writes the plan to a file and re-injects it after each compaction, so the plan survives a long session.
How do you check what loaded?
Run /context. It breaks the context window into system prompt, tools, MCP tools, subagents, memory files, skills and messages, so you can confirm which CLAUDE.md and rules files are present and how much each costs. Subdirectory CLAUDE.md files load on demand and do not appear there; when one loads, a Loaded line with its path shows in the terminal. If an instruction loaded but is ignored, the debugging docs suggest the wording is the problem, not the loading. The context window guide covers compaction and other ways to free space.
Keep shared conventions reusable
Per-package skills and prompts tend to be copied between repos. Kipt keeps prompts and Agent Skills as Markdown in a private GitHub repo you own and installs every skill into Claude Code with one npx skills add command, so the procedures you reuse across repos live in one place.