Claude Code in a monorepo: directory roots, CLAUDE.md, and not getting lost across packages
Published 2026-05-11 by Owner
Monorepos break one implicit assumption that most AI coding tools make: that the project root is the relevant context boundary. Start Claude Code at the root of a large monorepo and it will happily index your shared UI package, your backend API, your mobile app, your infra scripts, and three years of migrated legacy code — all of it treated as equally relevant context for whatever you asked about.
That’s not wrong exactly. It’s just slow, expensive, and often counterproductive.
The good news is that Claude Code’s session model is flexible enough to handle monorepos well. The bad news is that the defaults won’t get you there. You have to make deliberate choices about where to start each session, what CLAUDE.md files to write, and when to split work into separate sessions versus keeping it unified.
Root vs. package: where to start your session
The choice comes down to what kind of work you’re doing.
Start at the monorepo root when:
- The change crosses package boundaries — modifying a shared type, bumping a shared dependency, coordinating a breaking API change
- You need the agent to understand the relationship between packages (which depends on which, what the workspace graph looks like)
- You’re working on tooling that affects the whole repo: build configuration, CI scripts, root-level lint rules
Start inside a specific package when:
- The work is clearly local: a feature in
packages/web, a new endpoint inpackages/api, a component inpackages/ui - You want to keep costs down — the agent’s context is bounded by what it can see and read, and starting in a subdirectory limits the blast radius
- You’re running multiple parallel agents on independent packages (more on this below)
The tradeoff is real in both directions. A root-level session gives the agent cross-cutting vision. It can notice that your change to packages/shared/types/user.ts will break packages/api/routes/users.ts without being told. But it will also, on every file read, be aware of the mobile app’s navigation stack and your infra’s Terraform modules, neither of which matter for the task at hand.
A package-level session is focused and cheap. The agent understands the local code deeply because it’s not diluting context with irrelevant packages. The problem is that it cannot see across the boundary. Ask it to “update the User type” and it will update it correctly for this package, with no awareness that five other packages import the same type.
There’s no universal answer. Develop the habit of asking “is this change local to one package, or does it touch the seams between packages?” before starting a session.
CLAUDE.md per package
Claude Code reads the CLAUDE.md file in the directory where you start the session. That means the per-package CLAUDE.md strategy works naturally with the session-root approach above.
The pattern that works well:
monorepo-root/
├── CLAUDE.md ← describes the monorepo shape
├── packages/
│ ├── web/
│ │ └── CLAUDE.md ← web package conventions
│ ├── api/
│ │ └── CLAUDE.md ← api package conventions
│ └── shared/
│ └── CLAUDE.md ← shared package conventions
└── ...
Root CLAUDE.md should describe the overall structure. What workspace manager is in use (bun workspaces, pnpm workspaces, npm workspaces). How packages relate. Where to find shared types. The build and test commands that apply across the repo. What NOT to edit directly (generated files, vendored code, etc.).
A minimal but useful root CLAUDE.md:
# Monorepo structure
Bun workspaces. Packages under `packages/`. Apps under `apps/`.
- `packages/shared` — shared TypeScript types and utility functions. Imported by all other packages.
- `packages/ui` — shared React component library. Imported by `apps/web` and `apps/mobile-web`.
- `apps/api` — Hono API server. Runs on Node.
- `apps/web` — Next.js frontend.
## Cross-package changes
When modifying anything in `packages/shared`, run `bun run typecheck` from the root
to catch downstream type errors before editing call sites.
## Build
- `bun run build` from root builds all packages in dependency order.
- `bun run build --filter=apps/web` builds one app and its dependencies only.
- Tests: `bun run test` from root, or `bun test` from inside any package.
## Never edit
- `packages/shared/dist/` — generated, rebuilt on every build
- Any file with `// GENERATED` at the top
Package-level CLAUDE.md files describe the local conventions that don’t apply to the whole repo. The routing conventions in the API package. The component patterns specific to the UI library. The data model the mobile app uses that diverges from the web app.
When you start a session inside apps/api, Claude Code reads apps/api/CLAUDE.md. It does not automatically read the root CLAUDE.md — that only loads when you start at the root. This is actually fine: by the time you know you’re doing package-local work, you don’t need the monorepo-level context anyway. Keep each CLAUDE.md focused on its scope.
One practical note: Claude Code does not merge CLAUDE.md files from parent directories by default. If you want the agent to know both the root-level monorepo shape and the package-level conventions when starting from a package directory, you have two options: (1) start at the root and navigate the agent to the package, or (2) reference key root-level facts in the package CLAUDE.md — particularly the workspace command syntax and the cross-package type locations.
The cross-package edit problem
The hardest case: a shared type in packages/shared/types/user.ts needs a new field, and this type is imported in four different packages. Each package has call sites that will need updating.
Starting inside packages/shared and making the type change is straightforward. The hard part is the downstream cascade — finding every call site across the other packages and updating them consistently.
The workflow that handles this well:
-
Start the session at the monorepo root.
-
Instruct the agent to plan before touching anything. A prompt like: “The
Usertype inpackages/shared/types/user.tsneeds a new requiredtimezonefield. Before making any edits, identify every file across all packages that imports and uses this type. List them.” Let it do the read-and-search pass first. -
Review the list. The agent will miss things occasionally — particularly indirect usages, dynamic imports, or types that re-export from an intermediate file. Check the list before proceeding.
-
Execute in dependency order. Change the type definition first, then propagate outward. The agent can do this in one session if the cascade is manageable (under ~20 files). For larger cascades, break it into: first session changes the type and updates the immediate consumers in
packages/shared; subsequent sessions handle one downstream package each. -
Run the root-level typecheck between steps. Whatever your monorepo’s equivalent of
tsc --noEmitorbun run typecheckfrom root — run it after the shared-type change to see the full error list before touching call sites. This gives the agent a concrete list of failures rather than asking it to guess what broke.
The reason to plan-first is the same reason it matters in Cline’s Plan mode: if the agent immediately starts editing the type and then tries to find call sites, it’s working with partial information. The first edit might even introduce a syntax error that breaks the type resolution before the search completes. Plan the scope, verify it, then execute.
When separate sessions per package make sense
There are three clear cases for splitting into separate sessions rather than one unified root session:
The work is genuinely independent. Adding a new endpoint to apps/api while adding a new page to apps/web — if these don’t share any new types or data contracts, there’s no reason to run one session for both. Two separate sessions, each starting in their respective package directory, will run faster and produce more focused output.
Context contamination is a real cost. If apps/api is a large Node service and apps/mobile is a large React Native codebase, starting a session at root and asking it to work in apps/api means it’s carrying context about React Native navigation patterns it will never use. For long sessions this compounds. Package-level sessions are cheaper and the agent’s responses tend to be more on-point.
Parallel agents on independent packages. This is underused. If the work is genuinely parallel — say, upgrading a dependency in three packages that don’t depend on each other — running three Claude Code sessions simultaneously, one per package, is faster than one sequential session. Each session starts in its package directory, runs its changes, and you review the three diffs. The session model supports this naturally; nothing about Claude Code prevents multiple simultaneous sessions on the same repo as long as the packages don’t overlap.
How workspace tooling interacts (or doesn’t)
Claude Code doesn’t have built-in knowledge of your workspace manager. It will read your package.json workspaces field or pnpm-workspace.yaml if those files are in scope, but it won’t automatically understand the dependency graph or know which build --filter flags your tool supports.
This is where CLAUDE.md earns its keep. The root CLAUDE.md should document the key commands explicitly:
- The command to build all packages in dependency order
- The command to build a single package and its dependencies
- The command to run tests scoped to one package
- Any workspace-level type-checking command
Whether the repo uses bun workspaces, pnpm workspaces, turborepo pipelines, or Nx project targets doesn’t change how Claude Code works fundamentally. The agent can run shell commands. If your CLAUDE.md says “run turbo run build --filter=apps/web to build just the web app,” it will run that. If it says “run nx affected --target=test to run only the tests affected by your change,” it will run that too.
What you cannot rely on: the agent inferring these commands from your tooling config without being told. Even if your turbo.json is in context, the agent may not correctly infer the right filter syntax for your particular setup, especially for less common options. Spell it out in CLAUDE.md.
Tools like Nx and Turborepo also have remote caching and dependency-graph awareness that the agent doesn’t tap into programmatically — it just runs the commands you give it. The caching still works (it’s in the build tool), but the agent won’t make decisions based on the affected graph unless you explicitly tell it to run an affected query and then act on the results.
The thing that doesn’t work
One pattern that looks appealing but consistently disappoints: starting a session at root, asking the agent to “work in apps/api,” and expecting it to stay scoped. It won’t. Ask it to add an endpoint and it will read files in apps/api. But ask it a question about types and it will range across whatever packages are visible. Ask it to fix a test and it might look at how apps/web handles similar tests for “reference.”
Scoping the agent’s reading behavior within a root session is not reliably achievable through prompting alone. If the scope boundary matters — and in large monorepos it usually does — start the session in the right directory. That’s the reliable signal.
The per-package session approach requires more upfront thought about what kind of change you’re making. But it pays back in session quality, cost, and predictability. Monorepos reward deliberate context management in a way single-package repos don’t — and Claude Code’s session model, started at the right root with the right CLAUDE.md in place, handles it well.