In a large repository the first decision is made even before the request - by the choice of the starting directory. It determines which root and which configuration Claude considers working. Launching from the monorepo root suits a cross-package change; launching from a package narrows the search area and context. The key rule: a wrong starting scope is not compensated with a huge prompt. If the agent reads the wrong thing in the wrong place, the matter is usually in the entry point, not a lack of instructions.
Instructions in a large tree are laid out in layers, not dumped into one file. The root CLAUDE.md contains only the whole tree's invariants - the shared build, architecture, boundaries. Directory files load lazily when Claude reads files in the corresponding area. It helps to see such a layout once: the root CLAUDE.md, a cross-cutting rule in .claude/rules, and a per-package CLAUDE.md and skill next to the service code. This is cheaper and more precise than one giant root memory for all cases.
Lazy loading has a subtlety to remember. When a new file is created, the directory CLAUDE.md may not load in advance - Claude has not yet read files in that area. So a critical rule is better duplicated in the task or a path-scoped rule. A per-directory CLAUDE.md is good when rules belong to a package as a whole; .claude/rules with paths - for a cross-cutting pattern like all migrations. And if hundreds of layers start to duplicate, the shared procedure is moved into a plugin or skill.
A separate task is reducing useless reading, and this is a direct saving of context and money. It helps to set a deny on reading the generated and utility - dist, coverage, vendor, generated - and enable respectGitignore. Below is such a fragment. A caveat is mandatory: you do not block generated code if the task specifically requires checking a generated API or fixture - then you change the policy consciously and temporarily. And an LSP and code intelligence reduce bruteforce reads: definitions and references are often more precise than a repeated grep of the whole tree.
Sparse worktrees isolate work on part of a monorepo. The --worktree flag separates the branch and session, and a sparse checkout can include only the needed packages - but the dependency and test paths must stay available, or the run will break. The .worktreeinclude file copies the listed extra-untracked files into the managed worktree, and it is not used for secrets. So each parallel work on its own package goes in a light isolated tree rather than a full copy of a huge repository.
Access to sibling packages is given by --add-dir or permissions.additionalDirectories - but access does not mean automatic loading of all configuration sources from there. This is an important distinction: the agent gets the ability to read and edit a sibling package, but its CLAUDE.md, rules and settings are not picked up on their own. Before a cross-repo edit you determine the owner, version compatibility and commit strategy for each repository - otherwise the change spreads across foreign territory without coordination.
A cross-package change is worth running by an explicit plan, not by guesswork. The order is this: find the public contract and all its consumers; fix the compatibility and migration strategy; split ownership by packages; change the contract; update consumers and fixtures; run each package's focused tests; run the integration run over the affected graph; independently review the public API and rollout order. Each step relies on the previous rather than jumping to the finish through the whole tree at once.
Finally, the test structure is an interface for the agent. Package-level commands, affected tests and the full suite are documented separately: one test with no expected time and prerequisites makes the agent either waste hours or skip the check. Per-directory skills can encapsulate complex setup, but the command must leave a machine-readable exit code. The typical monorepo failure is a wrong entry point plus a huge prompt instead of context layers; the right move is to start in the needed area, lay out instructions in layers and close the noise.
monorepo/
├── CLAUDE.md # only the whole tree's invariants
├── .claude/rules/security.md # cross-cutting rule (paths)
├── services/api/
│ ├── CLAUDE.md # rules of the package as a whole
│ └── .claude/skills/deploy-staging/SKILL.md
└── apps/web/
└── CLAUDE.md # frontend conventions{
"permissions": {
"deny": ["Read(/dist/**)", "Read(/coverage/**)",
"Read(/vendor/**)", "Read(/generated/**)"]
},
"respectGitignore": true
}
// do not block generated if the task requires checking it