Claude Code in Monorepos: Scoped CLAUDE.md, Deny Rules, Worktrees
In a small repository, Claude Code's defaults work: one CLAUDE.md, launch from the root, let it search. In a monorepo with dozens of packages, the same defaults fill the context window with instructions for code you are not touching and with file reads you did not need. The cost shows up as more tokens per task and as instructions that get followed less consistently.
This guide follows a three-package monorepo (packages/api, packages/web, packages/shared) and applies the settings Anthropic documents for large codebases: where to launch, how to split CLAUDE.md, how to keep committed generated code out of reads, and how to keep worktrees small. For how the context window fills and compacts, see Inside Claude Code's Context Window; for running parallel sessions, see Parallel Coding with Git Worktrees.
Where you launch decides what loads
The directory you start claude from sets three things at once: which files Claude can read and edit without an extra grant, which CLAUDE.md files load at launch, and which project .claude/settings.json applies.
Start from the repository root and Claude can reach every file, but only the root CLAUDE.md loads at launch; each package's CLAUDE.md loads on demand once Claude reads, writes, or edits a file in that package. Start from packages/api/ and Claude can reach only that subtree until you grant more, and it loads packages/api/CLAUDE.md plus every ancestor's file, including the root. Instructions from packages/web/ never enter the session.
One trap: project settings are not inherited the way CLAUDE.md files are. A session started in packages/api/ reads packages/api/.claude/settings.json, not the root's, so each starting directory's settings file must be self-contained.
Quick reference
- Launch from the root for tasks that span packages; launch from a package for work scoped to it.
- Run
/contextand check Memory files to confirm whichCLAUDE.mdfiles actually loaded. - If a task started in one package needs a sibling, grant it with
--add-dir ../sharedinstead of relaunching from the root. - Files added with the
additionalDirectoriessetting are readable and editable, but theirCLAUDE.mdand skills do not load.
Remember this
Pick the launch directory per task: root for cross-package changes, the package for focused work. It is the cheapest scoping control you have.
Split CLAUDE.md by directory
A single root CLAUDE.md in a monorepo either grows to cover every package, spending context on rules unrelated to the task, or stays too generic to help. Anthropic's guidance is a two-level split: the root file holds rules that apply everywhere, such as commit conventions, and each package's file holds that package's stack conventions.
Keep each file short; the docs target under 200 lines per CLAUDE.md, because longer files cost context and reduce adherence. Imports with @path organize a long file but do not reduce its cost, since imported files also load at launch. If two files contradict each other, Claude may follow either one, so treat edits to these files like documentation changes and review them in pull requests.
1<!-- CLAUDE.md (root) -->2Run package scripts from the package directory, not the monorepo root.3Prefix commit subjects with the package name, e.g. `api: add rate limiting`.4Never edit files under packages/*/generated/; run `npm run codegen` instead.5 6<!-- packages/api/CLAUDE.md -->7Copy `.env.example` to `.env` before running anything.8Write queries with the Knex query builder; no raw SQL in route handlers.9Never edit a merged migration; add a new one.Quick reference
- Path-scoped rules in
.claude/rules/with apaths:glob are the central alternative: they load only when Claude works with matching files. - Use
claudeMdExcludes(for example"**/packages/web/**") to skip packages you never work in when launching from the root. - Move procedures that are needed only sometimes, like a release checklist, into skills; skills load only when relevant.
/doctor prompt-auditreports outdated or conflicting instructions across your instruction files.
Remember this
Root file for repository-wide rules, package files for stack conventions, skills for occasional procedures. Short files get followed.
Keep generated and vendored code out of reads
Claude's content searches already respect .gitignore, so node_modules/, dist/, and other ignored paths stay out of search results with no setup. The problem in a monorepo is committed noise: a vendored SDK, generated API clients, or checked-in build output that matches every search.
For those paths, add Read deny rules in permissions.deny. A .claudeignore file does nothing; Claude Code does not read it. Deny rules cover the built-in file tools, recognized Bash file commands such as cat and head when the path is an argument, and, on a best-effort basis, results from the built-in Grep and Glob tools. They do not cover a grep -r over a parent directory or a script that opens files itself.
End directory patterns with /**/* rather than /**: the rule then blocks everything inside the directory but still lets Claude list it or cd into it.
Quick reference
- Commit shared rules to
.claude/settings.jsonin each directory you launch from; personal rules go in.claude/settings.local.json. - Relative patterns like
Read(./**/vendor/**/*)anchor at the session's working directory; use//-absolute paths in a root-level local file if you launch from subdirectories. - For enforcement every session must obey, put the rules in managed settings, which nothing else can override.
- A code intelligence plugin (for example
typescript-lsp) lets Claude jump to definitions and references instead of reading through files to find them.
Remember this
Rely on .gitignore for ignored paths; use Read deny rules for committed noise. Never rely on .claudeignore.
Sparse worktrees and cross-package access
claude --worktree starts a session in a new Git worktree so its changes stay isolated from your main checkout. By default that worktree contains the whole repository. The worktree.sparsePaths setting uses Git sparse-checkout to write only the listed directories plus root-level files, so worktrees start faster and take less disk. This matters most when subagents each run in their own worktree.
List directories, not files, and include .claude if the worktree needs the root's settings or rules. Pair it with symlinkDirectories so every worktree links to the main checkout's node_modules instead of duplicating it.
1{2 "worktree": {3 "sparsePaths": [".claude", "packages/api", "packages/shared"],4 "symlinkDirectories": ["node_modules"]5 }6}Inside a worktree, the working directory is the worktree root, so settings load from the root's .claude/settings.json. Put deny rules you need in worktree sessions there too.
Quick reference
sparsePathsentries are relative to the repository root, whichever directory you launched from.- All worktrees in a session share one
sparsePathslist; if subagents need different packages, list them all. - Sparse checkout enables Git's
extensions.worktreeConfig; Claude Code removes it after the last worktree only if it added it. - Plain
git sparse-checkout seton your main checkout also works, but it changes your own working tree;sparsePathsscopes only Claude's worktrees.
Remember this
Use sparsePaths and symlinkDirectories so parallel worktrees stay small, and keep worktree-relevant settings in the repository root.
Hands-on: scope a three-package repo
Build the example monorepo and check each control with /context.
- Create
packages/api,packages/web, andpackages/shared, each with a shortCLAUDE.md, plus a rootCLAUDE.md. - Add
packages/api/vendor/sdk.jsand commit it. - In
packages/api/.claude/settings.json, add"Read(./**/vendor/**/*)"topermissions.denyand"../shared"topermissions.additionalDirectories. - Start
claudefrompackages/api/and run/context. - Ask Claude to open
vendor/sdk.js, then to edit a file inpackages/shared/.
Pass criterion: Memory files lists the root and api CLAUDE.md but not web's; the vendor read is refused; the edit in packages/shared/ succeeds without relaunching.
Quick reference
- If web's
CLAUDE.mdappears, check whether Claude touched a file underpackages/web/, which loads it on demand. - If the vendor read succeeds, check the rule with
/permissionsand confirm the settings file is in the directory you launched from. - Repeat step 4 from the repository root and compare which memory files load.
Remember this
Seeing /context change with the launch directory makes the loading rules concrete faster than reading about them.
Key takeaway
Scaling Claude Code to a monorepo is mostly about deciding what never enters the session. Launch from the narrowest directory that covers the task, keep the root CLAUDE.md to repository-wide rules with package files for the rest, rely on .gitignore for ignored paths and Read deny rules for committed noise, and keep worktrees sparse.
None of these require new tools, and each one is checkable: /context shows what loaded, /permissions shows which rules apply.
Practice (20 min): run the exercise above, then write the deny rules and sparsePaths you would commit for your own largest repository.
References
- Claude Code docs: Set up Claude Code in a monorepo or large codebase — checked Oct 17, 2026
- Claude Code docs: How Claude remembers your project (CLAUDE.md) — checked Oct 17, 2026
- Claude Code docs: Configure permissions — checked Oct 17, 2026
Shoaib Hossain
Author, Core Concept LearningRelated Articles
Explore this topic