A million-line tree will eat your context window if you let it. Running Claude Code in a monorepo with defaults tuned for small repos means every session starts by paying for instructions and files that have nothing to do with the task. What follows is our decision-first playbook: where to launch Claude Code per task shape, which instructions load, which grants to add, and how to search cheaply.
What you’ll learn
- Pick the launch directory that scopes a Claude Code monorepo session to the right subtree
- Layer CLAUDE.md files so per-package instructions load lazily instead of all at once
- Scope rules to path globs and mute other teams’ instructions with
claudeMdExcludes - Grant sibling-package access without losing coherence on cross-package refactors
- Cut file-read costs with deny rules, code intelligence plugins, and Explore subagents
Prerequisites
- Daily, hands-on Claude Code use — we skip installation and CLAUDE.md basics
- A recent Claude Code version (2.1.x era) for
claudeMdExcludes,.claude/rules/path scoping, andworktree.sparsePaths - A workspace with multiple packages — examples use
packages/api,packages/web, andpackages/shared, but the same patterns apply to any large single tree
The two context cost centers
A monorepo drains context through two separate channels. The first is instructions: every CLAUDE.md, rules file, and skill description that loads into the session. The second is file reads: every file Claude opens while exploring, and every line of tool output that follows.
Most advice treats these as one problem. They are not.
Instructions are a launch-time and lazy-load cost you control with structure and settings. File reads are a runtime cost you control with search strategy and permissions.
Both draw from the same context budget, so a working Claude Code monorepo setup needs levers on both. The next three sections handle instructions. The rest handle reads and the cross-package coherence problem that scoping creates.
Pick your Claude Code monorepo launch directory
Where you start claude is the single biggest scoping decision. The launch directory determines three things at once: which files Claude can read and edit without extra grants, which CLAUDE.md files load at startup, and which .claude/settings.json applies.
| Start from | File access | CLAUDE.md loaded at launch |
|---|---|---|
| Repository root | Every file | Root only; subdirectory files load on demand |
| A subdirectory | That subtree only, until you grant more | That directory’s plus every ancestor’s |
Match the launch directory to the task shape:
- Single-package fix: start in the package. You get a hard file-access fence, that package’s conventions, and none of its 39 siblings.
- Cross-package refactor: start in the primary package and grant siblings (covered below), or start at the root and lean on exclusions.
- Repo-wide question: start at the root and delegate discovery to subagents so raw reads stay out of your thread.
Project settings do not inherit the way CLAUDE.md does. A .claude/settings.json at the repository root applies only when you start from the root. Launch from packages/api/ and the root’s deny rules, hooks, and permissions are simply not there — each package’s settings file must be self-contained.
That asymmetry is the gotcha that bites teams first. Memory files layer up the tree; settings files do not. If a Read deny rule matters everywhere, it has to exist in every directory people launch from.
Layer CLAUDE.md by directory
Claude Code walks up the tree from your working directory, loading every CLAUDE.md and CLAUDE.local.md it finds — the same memory system that powers single-package repos. Discovered files concatenate — nothing overrides — ordered filesystem-root-down, so the file closest to your launch directory reads last. Within a directory, CLAUDE.local.md appends after CLAUDE.md.
Files below the working directory are the interesting part. They do not load at launch — they load on demand, when Claude reads files in those subdirectories. That mechanism makes per-package CLAUDE.md files context-cheap: packages/web/CLAUDE.md costs nothing during an API-only session started at the root.
The split that follows from the mechanics: the root file is a repository map plus universal rules, and each package file holds local conventions. Treat the root file as a contract, not a wiki — a map, not a manual.
# Monorepo map
- packages/api — Fastify REST API (TypeScript)
- packages/web — Next.js frontend
- packages/shared — types and utilities used by both
## Universal rules
- Run commands from the package directory, not the root
- Conventional commits (feat:, fix:, chore:)
- Never edit generated files (*.generated.ts)
# API package
- Fastify 5, TypeScript strict, Zod for validation
- Run tests: `npm test` (from this directory)
- Migrations live in src/db/migrations — never edit applied ones
- All endpoints require input validation before handler logic
Ancestor CLAUDE.md files load in full at launch, regardless of length. Keep each file under 200 lines — shorter files also get better adherence. Directory owners maintain their own file and review CLAUDE.md edits in PRs like documentation.
Claude Code strips block-level HTML comments (<!-- ... -->) from CLAUDE.md before injection. Use them for maintainer notes — “last reviewed 2026-06”, “see PR #412 for why” — at zero context cost.
One compaction asymmetry matters in long monorepo sessions. After /compact, Claude re-reads the project-root CLAUDE.md from disk and re-injects it. Claude Code does not re-inject nested CLAUDE.md files — they reload only the next time Claude reads a file in that subdirectory.
If a mid-task session goes strange after compaction, this is a likely reason. The auto-compact survival guide covers the recovery patterns.
Path-scoped rules and exclusions
Per-directory CLAUDE.md is not the only way to scope instructions. Rules files in .claude/rules/ load at launch by default, but a paths: frontmatter glob makes them load only when Claude works with matching files:
---
paths:
- "packages/api/**/*.ts"
---
# API rules
- All endpoints must include input validation
Choose between the two by ownership. Per-directory CLAUDE.md lives with the code and its owners, versioned alongside the package. Centralized path-scoped rules win when one convention spans scattered paths — a single migrations rule matching **/migrations/** beats copying it into eight packages.
The broader extension point decision matrix covers where skills and plugins fit into this same choice.
The memory docs say path-scoped rules trigger when Claude reads files matching the pattern, not on every tool use. So a rule scoped to packages/api/** may not be in context yet if Claude creates a brand-new file at packages/api/src/users.ts without first reading anything under that glob. Put must-hold constraints somewhere that loads regardless: the CLAUDE.md of the directory you launch from, or a hook.
Exclusions solve the opposite problem. Starting from the root, every subdirectory’s CLAUDE.md loads as soon as Claude reads a file there — including packages you never intentionally touch. The claudeMdExcludes setting skips them by glob:
{
"claudeMdExcludes": [
"**/packages/admin-dashboard/**",
"**/packages/legacy-*/**"
]
}
Globs match against absolute paths, so start patterns with **/. Excluding a package’s tree skips its CLAUDE.md and its rules files, and arrays merge across settings scopes.
Two caveats: managed-policy CLAUDE.md cannot be excluded, and the docs are explicit that this is static, not a per-task switch. To focus on a different package tomorrow, launch Claude from that package instead of editing exclusions.
Search without reading half the repo
Claude Code has no index. It navigates a codebase the way an engineer would: traverse the file system, grep for what it needs, follow references. There is no embedding index to go stale, so it always searches the code as it is on disk right now.
The tradeoff: agentic search works best when Claude has enough starting context to know where to look — which is exactly what the layered CLAUDE.md map provides.
Think of retrieval as three layers with different prices. File search and content search are cheap — whether they run through the Glob and Grep tools or through find and grep in Bash, depending on platform, they return paths and matched lines, not file contents. Read is the expensive confirm step.
Block expensive reads up front
The named failure pattern in the official docs is “the infinite exploration”: Claude reads hundreds of files and fills the context. Three mechanisms block it before prompting even starts.
First, content searches respect .gitignore by default, so node_modules/, dist/, and build/ stay out of results for free. Second, Read deny rules block checked-in noise that git does not ignore — vendored SDKs and committed generated code:
{
"permissions": {
"deny": [
"Read(./**/dist/**/*)",
"Read(./**/*.generated.*)",
"Read(./**/vendor/**/*)"
]
}
}
These deny rules cover the built-in file tools and recognized Bash file commands (cat, head, grep, find) when a denied path is an argument. A Bash grep -r or find over a directory that contains denied files still includes them in its output, and subprocesses that open files themselves aren’t covered, so treat these rules as a cost control, not a security boundary.
Third, replace scans with symbol lookups. Code intelligence plugins connect Claude to a language server, so jump-to-definition and find-references replace many grep-plus-Read round trips:
/plugin install typescript-lsp@claude-plugins-official
The official marketplace covers TypeScript, Python, Go, Rust, and other common languages; each requires the language server binary locally. Enable it repo-wide through the enabledPlugins project setting. Exclusions keep irrelevant content out; code intelligence keeps Claude from scanning what remains.
Delegate bulk discovery to subagents
For discovery that still needs many reads, delegate it. An Explore subagent greps and reads inside its own context window and returns a summary — the raw file contents never enter your thread. This is the subagents-as-context-firewalls pattern applied to monorepo scale:
Use subagents to investigate how packages/api handles token refresh, and whether packages/shared has any existing OAuth utilities I should reuse. Report file paths and function signatures, not full file contents.
Note the last sentence of the prompt. It is an output contract: paths and signatures, not contents. Without it, the subagent’s summary can re-pollute your thread with the very file dumps you delegated to avoid.
Keep cross-package changes coherent
Scoping creates a new problem: the change that spans packages. When you start from packages/api/, Claude cannot touch packages/shared/ without a grant. Two mechanisms exist, and they load memory differently:
| Added with | Loads CLAUDE.md and rules | Loads skills |
|---|---|---|
additionalDirectories setting | Never | Never |
--add-dir flag or /add-dir command | Only with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 | Yes |
Commit grants everyone needs to the package’s settings file, under permissions: "permissions": { "additionalDirectories": ["../shared"] }. Relative paths resolve against the directory you start Claude from. Use the flag for one-offs, with the environment variable when you want the sibling’s conventions loaded too:
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 claude --add-dir ../shared
Plan the change in one session
Access is the easy half. Coherence is the hard half: a shared-type change re-derived per package produces three subtly different migrations.
The official guidance gives two techniques. Give Claude the whole change in one session, so the decisions behind each edit stay consistent. And save the plan to a file before editing, because a long cross-package session compacts along the way — the saved plan survives where conversation history may not.
Here is the canonical prompt combining both, ready to adapt:
We’re renaming the
InvoiceStatustype in packages/shared/src/types/billing.ts from a string union to an enum. First, in plan mode: find every import of InvoiceStatus across packages/api and packages/web, and write the full migration plan to docs/plans/invoice-status-enum.md — every file, every call site, the order of edits, and the per-package test commands to run. Then migrate package by package, checking off the plan file as you go.
As a procedure:
- 01
Explore in a subagent
Fan discovery out to an Explore subagent: where the type is defined, every import, every call site. Only the summary enters your thread.
- 02
Write the plan to a file
Use plan mode, and insist the plan lands on disk, not just in conversation. A plan worth approving names every file, the edit order, and the verification command per package.
- 03
Edit the shared package first
Change
packages/sharedand get it compiling before touching any consumer. The type change anchors every downstream decision. - 04
Migrate call sites package by package
Work through consumers in the plan’s order, checking off the plan file as each package completes. After any compaction, Claude re-reads the plan instead of re-deriving it.
- 05
Verify per package, not repo-wide
Run each package’s own test command —
npm testfrom the package directory, orturbo run test --filter=api/nx affected -t testif your tooling supports affected-only runs. Whole-workspace runs are slower and flood context with irrelevant output. Package-scoped commands are the monorepo form of a tight verification loop.
The per-package CLAUDE.md is where those exact test commands belong. Claude should never have to guess whether this package uses vitest or jest.
Shrink worktrees to the task
A Claude Code monorepo setup makes --worktree sessions expensive: a full checkout per worktree, plus a dependency install. The worktree.sparsePaths setting uses git sparse-checkout so worktree sessions — including subagent worktrees — check out only the listed directories plus root-level files. This config follows the combined per-package example in the official large-codebases guide, and brings together the committed settings covered above:
{
"worktree": {
"sparsePaths": [".claude", "packages/api", "packages/shared"],
"symlinkDirectories": ["node_modules"]
},
"permissions": {
"additionalDirectories": ["../shared"],
"deny": ["Read(./**/dist/**/*)", "Read(./**/build/**/*)"]
}
}
Two details are load-bearing. Include .claude in sparsePaths, or the root’s settings, rules, and skills will not exist inside the worktree. And symlinkDirectories links node_modules from the main checkout instead of duplicating the install.
One caveat from the docs: after creation, the working directory is the worktree root. Permission rules and hooks that must apply inside worktrees therefore have to live in the repository root’s .claude/settings.json. Sparse worktrees are the monorepo-sized variant of the parallel worktree workflow — same isolation, a fraction of the disk and setup cost.
When layering stops scaling
Per-directory files assume directory owners who maintain them. Sometimes packages multiply faster than owners: stale files, drifting conventions, nobody owning the root. At that point the docs recommend moving conventions into skills and plugins owned by a platform team.
Per-directory skills already behave well in a monorepo: packages/api/.claude/skills/api-testing/SKILL.md loads on demand only when relevant, and packages/web/ skills never load during API work. Skills can also scope by paths: frontmatter glob, so a migrations skill at the root can apply only under **/migrations/**.
One discovery caveat: launched from the root, skills from every touched subdirectory accumulate, and when there are many, some skills lose their descriptions from the listing entirely. Lead each description with the keywords a request would contain.
Beyond that, a SessionStart hook can recommend the right plugin for the launch directory — a guardrail rather than a suggestion. And if your org runs a code-search index behind an MCP server, weigh it against the context cost of MCP servers before adding it to every session.
Whatever Claude Code monorepo configuration you land on, review it. Anthropic recommends revisiting CLAUDE.md and configuration periodically, especially after major model releases — rules written to work around an old model’s habits become pure overhead.
In a large organisation this is a platform-team job, not something each engineer should work out alone. Left to individuals, you get root settings that silently vanish when someone launches from a package, deny rules copied into some packages and not others, and exclusions nobody remembers adding. When we roll Claude Code out through Enable, the per-package settings, the root worktree copy and the shared skills or plugin get defined once, owned by one team and reviewed like any other config.
Next steps
- Context budgeting in Claude Code — the accounting framework behind every lever above
- Subagents as context firewalls — output contracts and isolation patterns for the Explore delegation used here
- Parallel Claude Code with git worktrees — the full worktree workflow that
sparsePathsscales down - Official guide: Claude Code in large codebases — the reference for every setting covered here
- Official memory documentation — load order, imports, and rules mechanics in full