teachyou.ai academy
← All posts
Claude Codemonorepodeveloper toolsCLAUDE.mdagentic coding

Using Claude Code in a Monorepo

Pramod Dutta · Jul 7, 2026 · 12 min read

Claude Code monorepo setups fail in a predictable way: you ask it to fix a bug in packages/api, and it also touches packages/web, runs the wrong test command, or reads three unrelated packages before it finds the file you meant. None of that is a model problem. It's a context and configuration problem, and it's fixable with a handful of concrete changes to how you structure CLAUDE.md files, scope permissions, and launch the agent.

This guide walks through a real setup: a monorepo with apps/web, apps/api, packages/ui, packages/db, and a root workspace file. By the end you'll have a layout that keeps Claude Code fast, accurate, and contained to the package you're actually working on.

Why monorepos break the default Claude Code setup

Claude Code reads a CLAUDE.md file at the start of a session to get project context: build commands, conventions, architecture notes. In a single-package repo, one file at the root is enough. In a monorepo, a single root CLAUDE.md has to describe five different build systems, five different test runners, and five different sets of conventions at once. Two things go wrong:

  • Context bloat. Every session loads the whole file, even if you're only touching one package. A 400-line root CLAUDE.md covering every workspace eats context budget before you've typed a single instruction.
  • Command ambiguity. If the root file says "run tests with npm test" but apps/api actually uses pytest and packages/ui uses vitest, Claude Code will guess, get it wrong in one of the three, and burn a turn re-discovering the right command.

The fix isn't one giant file. It's a root file that acts as a map, plus scoped files inside each package that only load when you're actually working there.

Structuring CLAUDE.md for a multi-package repo

Claude Code supports nested CLAUDE.md files. When you start a session with your working directory inside apps/api, Claude Code picks up both the root file and the apps/api/CLAUDE.md file, but it treats the root one as a lightweight index rather than a full manual.

Root CLAUDE.md:

# Monorepo map

This is a pnpm workspace with four packages. Read the package-level
CLAUDE.md before touching code inside it; do not assume conventions
from one package apply to another.

- `apps/web`: Next.js frontend. See apps/web/CLAUDE.md.
- `apps/api`: FastAPI backend. See apps/api/CLAUDE.md.
- `packages/ui`: shared React component library, published internally.
- `packages/db`: Prisma schema and migrations, consumed by apps/api only
  through the generated client, never imported directly by apps/web.

Root-level commands:
- `pnpm install` installs all workspaces.
- `pnpm -w build` builds everything; prefer `pnpm --filter <pkg> build`
  for a single package during iteration.
- Never run `pnpm -w test` unless explicitly asked; it runs the full
  suite across all five packages and takes 12+ minutes.

Notice what this root file does not contain: no test commands per package, no lint rules, no architecture detail. It's a directory, and it sets one important guardrail (don't run the full test suite by default) that would otherwise be invisible until Claude Code accidentally does it.

Package-level apps/api/CLAUDE.md:

# apps/api (FastAPI backend)

- Python 3.12, managed with `uv`. Never use `pip install` directly;
  use `uv add <package>` so pyproject.toml and the lockfile stay in sync.
- Run the dev server: `uv run uvicorn app.main:app --reload --port 8000`.
- Run tests: `uv run pytest`. Run a single file with
  `uv run pytest tests/test_orders.py -x`.
- Database access goes through `packages/db`'s generated client only.
  Do not write raw SQL in this package; add a Prisma query or, if the
  query is genuinely too complex, ask before writing raw SQL.
- Routes live in `app/routers/`, one file per resource. New routes
  need a corresponding test in `tests/` before being considered done.

This file only loads when Claude Code's working context touches apps/api. That keeps the token cost proportional to what you're actually doing, and it removes ambiguity: there's exactly one place that says how to run tests in this package, and it's the file that's closest to the code.

Do the same for apps/web/CLAUDE.md, packages/ui/CLAUDE.md, and packages/db/CLAUDE.md. Each one should answer three questions in under 30 lines: how do I run this, how do I test this, and what's the one convention a newcomer would get wrong.

Launching Claude Code scoped to a package

The biggest lever you have in a monorepo isn't configuration, it's working directory. If you cd apps/api before starting a session, Claude Code's file tools, glob searches, and default read/write operations all anchor there. It won't wander into apps/web unless you explicitly point it there, because its notion of "the project" is scoped to where it was launched.

cd apps/api
claude

versus running from the monorepo root and hoping instructions like "fix the bug in the API" are specific enough. They usually aren't. "Fix the bug in the API" from the root can result in Claude Code grepping across all five packages for anything matching your bug description, which is slower and occasionally wrong when apps/web and apps/api share a function name.

If you need to work across two packages in the same session (a shared type change in packages/ui that ripples into apps/web), launch from the root but be explicit in your first message about scope:

Update the Button component prop types in packages/ui/src/Button.tsx
to add a `loading` state, then update every usage in apps/web that
passes the old props. Do not touch apps/api or packages/db.

The explicit "do not touch" line matters more than it looks. Claude Code will use its judgment to explore related code, and in a monorepo "related" can mean "any package that imports this," which is correct behavior for a refactor but wasted work for a scoped fix.

Permissions and settings.json per workspace

Claude Code's settings.json supports permission rules scoped by directory, which matters a lot in a monorepo where you want different tooling allowed in different packages. A common pattern: allow uv run pytest unprompted in apps/api, but keep database migration commands (uv run prisma migrate deploy, anything touching packages/db) gated behind manual approval everywhere.

Project-level .claude/settings.json at the repo root:

{
  "permissions": {
    "allow": [
      "Bash(pnpm --filter apps/web build)",
      "Bash(pnpm --filter apps/web dev)",
      "Bash(uv run pytest*)",
      "Bash(uv run uvicorn*)",
      "Read(./apps/**)",
      "Read(./packages/**)"
    ],
    "deny": [
      "Bash(uv run prisma migrate deploy*)",
      "Bash(rm -rf*)"
    ]
  }
}

This does two things for a monorepo specifically. First, it lets iteration inside apps/api and apps/web happen without a permission prompt every time you run tests, which is the single biggest source of friction in a multi-package repo where you're running the same handful of commands dozens of times a session. Second, it hard-blocks the one class of command that's actually dangerous across a monorepo: schema migrations, which affect every package that reads from the same database, and destructive filesystem operations that could take out a sibling package by accident.

If different contributors work in different packages, you can push package-specific settings into apps/api/.claude/settings.json and apps/web/.claude/settings.json so each package's contributors only see the permission rules relevant to their corner of the repo, without the root file becoming a giant allowlist for tools nobody in that package uses.

Keeping searches fast with workspace-aware tooling

Grep and glob searches inside a large monorepo are one of the most common places token budget disappears. If Claude Code searches for a function name and the monorepo has node_modules, build output, and five packages' worth of generated code, a naive search returns hundreds of irrelevant hits before it finds your actual match.

Two fixes, both cheap:

Add a `.claude/ignore` pattern list (or rely on .gitignore, which Claude Code respects by default) that excludes generated artifacts:

node_modules/
.next/
dist/
build/
packages/db/prisma/generated/
**/*.min.js

Point searches at the package, not the repo. Instead of asking "find where formatCurrency is used," which triggers a repo-wide grep, ask "find where formatCurrency is used in apps/web" if that's genuinely where you know it lives. When you don't know which package owns something, that's a legitimate repo-wide search, but most of the time in day-to-day work you already know the package, and saying so saves a full search pass.

For very large monorepos (50+ packages), consider maintaining a lightweight WORKSPACE.md or extending the root CLAUDE.md map with a table of which packages import which, so Claude Code doesn't have to reconstruct the dependency graph by reading every package.json when a change might ripple outward.

Handling shared packages and cross-package changes

The hardest monorepo case is a change to a shared package (packages/ui, packages/db, a shared packages/types) that has downstream consumers. Claude Code handles this well if you give it the dependency direction explicitly, because otherwise it either stops at the boundary of the package it started in or over-eagerly edits every consumer without being asked.

State the blast radius up front:

packages/types/src/order.ts defines the Order type. It's imported by
apps/api (via the generated client, not directly) and apps/web
(directly, in about a dozen files). I'm adding a `refundedAt` field.
Update the type, then search apps/web for places that construct an
Order object literal and add the field there too. Don't change
apps/api; the Python side has its own Pydantic model that's kept in
sync manually and I'll update that myself.
</br>

That instruction does the job a monorepo-specific dependency graph would otherwise need to do: it tells Claude Code exactly how far the change should propagate and where the boundary is, instead of relying on it to infer ownership boundaries from import statements alone, which works but costs an extra exploration pass.

For genuinely large cross-cutting changes (a rename that touches 40 files across three packages), it's worth splitting the work into package-scoped sessions rather than one giant session: fix packages/ui first, verify it builds and its own tests pass, then move to apps/web with a fresh session that only needs to know the new shape exists, not the history of how you got there. This keeps each session's context focused and makes it much easier to review the diff package by package.

Testing across packages without running everything

A monorepo's biggest workflow tax is the "did I break something else" question. Running the full test suite after every change is correct but slow, and if your root CLAUDE.md doesn't say otherwise, Claude Code will sometimes default to the safest-looking option, which is running everything.

Be explicit about the testing strategy in the root file:

## Testing strategy

- After changing a single package, run that package's tests only:
  `pnpm --filter <package> test`.
- Only run the full suite (`pnpm -w test`) before opening a PR, or if
  you changed a package that three or more other packages depend on
  (packages/ui, packages/types, packages/db).
- CI runs the full suite on every PR regardless, so local full-suite
  runs are a courtesy check, not a requirement, for low-blast-radius
  changes.

This turns a vague judgment call ("should I run everything?") into a rule Claude Code can apply consistently, and it matches how most engineers actually work in a monorepo: fast scoped checks locally, full verification in CI.

A minimal working layout

Putting it together, a monorepo that plays well with Claude Code looks like this:

.
├── CLAUDE.md                 # map + root guardrails, under 60 lines
├── .claude/
│   └── settings.json          # repo-wide permission rules
├── apps/
│   ├── web/
│   │   └── CLAUDE.md          # Next.js specifics, ~30 lines
│   └── api/
│       └── CLAUDE.md          # FastAPI specifics, ~30 lines
└── packages/
    ├── ui/
    │   └── CLAUDE.md          # component library conventions
    └── db/
        └── CLAUDE.md          # schema, migration rules, generated client notes

None of these files individually need to be long. The value comes from separation: each one answers questions only for the part of the repo it lives in, so a session started inside apps/api never pays the token cost of packages/ui's conventions, and a session that does need to span packages gets an explicit map instead of five overlapping sets of instructions competing for relevance.

FAQ

Does Claude Code automatically load every nested CLAUDE.md in a monorepo? No. It loads the root file plus the nested files along the path to your working directory. If you launch from apps/api, you get the root map and apps/api/CLAUDE.md, not apps/web/CLAUDE.md or packages/ui/CLAUDE.md, unless the session's work later takes it into those directories.

Should I put build and test commands in the root CLAUDE.md or the package-level one? Package-level. The root file should only hold information that's true for the whole repo: the package manager, the overall map, and any hard guardrails like "never run the full test suite unprompted." Per-package build and test commands belong in that package's own file, where they're unambiguous.

How do I stop Claude Code from editing a package I didn't ask about? State the boundary explicitly in your instruction ("don't touch apps/api") and, if it's a recurring boundary, add a deny rule in .claude/settings.json scoped to that package's write paths. Explicit scoping in the prompt handles one-off cases; permission rules handle the ones you want enforced every session.

Is it worth splitting a monorepo-wide refactor into separate sessions per package? For anything touching more than two or three packages, yes. A fresh session per package keeps context focused on that package's conventions and avoids the accumulated context from earlier packages leaking into decisions about later ones. Verify each package's build and tests before moving to the next.

What if two packages use the same command name but mean different things by it, like `test` in a JS package versus a Python package? This is exactly what package-level CLAUDE.md files solve. Each file states its own literal test command (pnpm --filter apps/web test versus uv run pytest), so Claude Code never has to guess which test you mean based on which directory it happens to be reasoning about at the time.

Using Claude Code in a Monorepo · TeachYou Academy