teachyou.ai academy
← All posts
Claude Code

Claude Code for Monorepos: Managing Multiple Packages

Ira Menon · Jun 22, 2026 · 14 min read

Why Monorepos Break Naive AI Coding Assistants

A monorepo looks simple from the outside: one Git repository, one clone command, one place to cd into. In practice, it is closer to a small city. You might have a web package built on Next.js, an api package running Express or Fastify, a shared package full of types and utilities, a mobile package in React Native, and a handful of internal tools that nobody remembers the purpose of anymore. Each of these has its own dependencies, its own build step, and sometimes its own testing framework.

This structure is exactly where AI coding assistants tend to fall apart. A tool that treats the repository as one undifferentiated blob of files will happily read your entire node_modules tree, confuse the tsconfig.json in packages/web with the one in packages/api, or "fix" a bug in packages/shared/src/utils.ts in a way that breaks three downstream packages you didn't mention. The failure mode is rarely a crash. It is quieter and worse: subtly wrong context that produces confidently wrong code.

Claude Code, Anthropic's terminal-based coding agent, was built with this problem in view. It does not require a special "monorepo mode," but it gives you the primitives to make monorepo work reliable: per-package CLAUDE.md files, scoped context loading, custom subagents, permission rules keyed to directories, and hooks that can enforce package boundaries automatically. None of this is exotic. It is mostly disciplined configuration. But the difference between a monorepo where Claude Code is a genuine multiplier and one where it causes more cleanup work than it saves usually comes down to whether that configuration exists at all.

This article walks through the concrete setup: how Claude Code discovers and loads context in a multi-package repository, how to write CLAUDE.md files that scope correctly, how to constrain file access per package, how to handle cross-package refactors without it wandering into packages it shouldn't touch, and how to keep the whole thing fast as the repo grows. If you are running a workspace with more than two or three packages, this is the setup you want before you hand Claude Code anything nontrivial.

How Claude Code Discovers Context in a Multi-Package Repo

Claude Code reads CLAUDE.md files hierarchically. When you start a session, it looks for a CLAUDE.md at the repository root, and it will also pick up CLAUDE.md files in the current working directory and parent directories relative to wherever you launched the session or wherever you are actively editing. This matters enormously in a monorepo, because it means you are not stuck writing one giant root-level file that tries to describe five unrelated packages at once.

A typical monorepo layout looks something like this:

my-monorepo/
  CLAUDE.md
  package.json
  turbo.json
  packages/
    web/
      CLAUDE.md
      package.json
      src/
    api/
      CLAUDE.md
      package.json
      src/
    shared/
      CLAUDE.md
      package.json
      src/
    mobile/
      CLAUDE.md
      package.json
      src/

The root CLAUDE.md should describe things that are true everywhere: the package manager (npm, pnpm, yarn), the monorepo tool (Turborepo, Nx, Lerna, or plain workspaces), the overall architecture, and any rules that apply globally, like "never commit directly to main" or "all packages use TypeScript strict mode." Each package-level CLAUDE.md then describes what is specific to that package: its framework, its test runner, its build command, its conventions, and — critically — what it should never import from or export to.

Here is a reasonable root-level CLAUDE.md:

# my-monorepo

This is a pnpm workspace monorepo managed with Turborepo. Packages live under `packages/*`.

## Global rules
- Package manager is pnpm. Never use npm or yarn commands.
- Run `pnpm turbo run build --filter=<package>` to build a single package.
- Run `pnpm turbo run test --filter=<package>` to test a single package.
- Shared types and utilities live in `packages/shared`. Do not duplicate logic
  that already exists there.
- Never edit generated files under `**/dist/` or `**/.turbo/`.

## Package map
- `packages/web` — Next.js customer-facing app
- `packages/api` — Fastify backend API
- `packages/shared` — shared TypeScript types, validation schemas, utils
- `packages/mobile` — React Native app (Expo)

When a task touches more than one package, check each package's own
CLAUDE.md before editing.

And a package-scoped one, for packages/api/CLAUDE.md:

# packages/api

Fastify + TypeScript backend. Talks to Postgres via Drizzle ORM.

## Conventions
- Route handlers live in `src/routes/`, one file per resource.
- Validation schemas come from `packages/shared/src/schemas`, imported as
  `@myrepo/shared`. Do not redefine schemas locally.
- Every route handler must have a corresponding test in `src/routes/__tests__/`.
- Database migrations go through `pnpm drizzle-kit generate`, never hand-edited.

## Testing
- `pnpm turbo run test --filter=api` runs the Vitest suite.
- Integration tests need a running Postgres; use `pnpm docker:db` first.

This layering means that when you open a Claude Code session inside packages/api and ask it to add an endpoint, it loads both the root rules and the API-specific rules, but it does not load the React Native conventions from packages/mobile that are irrelevant to the task. Less noise in context means fewer wrong assumptions in the output.

Scoping Sessions to a Single Package

The single highest-leverage habit for monorepo work with Claude Code is starting your session from inside the package you actually intend to change, rather than from the repo root every time. If you run claude from packages/web, the working directory itself becomes a scoping signal — file searches, glob patterns, and relative paths all naturally stay inside that package unless you explicitly ask Claude Code to look elsewhere.

That said, plenty of real tasks are cross-package by nature — a shared type changes shape and three consumers need updating. For those, it helps to be explicit in your prompt about which packages are in scope, rather than relying on Claude Code to infer it:

I changed the `Invoice` type in packages/shared/src/types/invoice.ts to add
a required `taxRegion` field. Find every place in packages/api and
packages/web that constructs an Invoice object and update it to include
taxRegion. Do not touch packages/mobile — that consumer is on hold.

Naming the excluded package explicitly is not paranoia — it is the same instinct as telling a new hire "don't touch billing yet, we're mid-migration." Claude Code is good at following explicit scope; it is not good at guessing scope you didn't state.

Using Settings to Enforce Directory Boundaries

Prompts are advisory. If you actually want a hard boundary — for instance, a compliance-sensitive package that no agent session should edit without a human explicitly approving each change — that belongs in .claude/settings.json, not in a CLAUDE.md instruction. Claude Code's permission system lets you allow, ask, or deny tool use based on path patterns.

A settings file scoped to protect a sensitive package might look like this:

{
  "permissions": {
    "allow": [
      "Read(packages/**)",
      "Edit(packages/web/**)",
      "Edit(packages/api/**)",
      "Edit(packages/shared/**)",
      "Bash(pnpm turbo run test --filter=*)",
      "Bash(pnpm turbo run build --filter=*)"
    ],
    "ask": [
      "Edit(packages/billing/**)",
      "Bash(pnpm turbo run deploy*)"
    ],
    "deny": [
      "Edit(**/dist/**)",
      "Edit(**/.turbo/**)",
      "Edit(**/node_modules/**)"
    ]
  }
}

This configuration lets Claude Code read anything in the workspace for context, freely edit the three packages you trust it with day-to-day, but stop and ask before touching packages/billing or running a deploy command. The deny rules for dist, .turbo, and node_modules matter more than they look — without them, an agent chasing down a bug can waste an entire turn "fixing" a compiled output file that gets overwritten on the next build anyway, or worse, propose a diff against a node_modules dependency instead of the source package that actually needs the change.

You can nest a .claude/settings.json per package as well, so packages/billing/.claude/settings.json layers stricter rules on top of the root settings when a session is scoped there. This mirrors the same hierarchical loading behavior as CLAUDE.md, applied to permissions instead of instructions.

Keeping Cross-Package Refactors From Wandering

The riskiest monorepo task is the one that legitimately spans packages: renaming a shared function, changing an API contract, upgrading a shared dependency. Claude Code will do a reasonable job at these if you give it a search strategy up front instead of letting it wander file by file.

A useful pattern is to ask it to build a plan before touching anything:

Before editing anything, grep the whole monorepo for usages of
`calculateShippingCost` from packages/shared/src/pricing.ts. List every
file and package that imports it, and for each one tell me whether the
call site needs to change given the new signature
(origin: string, destination: string, weightKg: number). Wait for my
confirmation before editing.

This two-step approach — survey, then confirm, then edit — costs one extra round trip but saves you from a diff that touches twelve files across four packages when only seven of those call sites actually needed a change. For genuinely large refactors, it's worth explicitly asking Claude Code to run the affected packages' test suites after each package's edits are done, rather than after the entire multi-package change, so a broken change in packages/shared is caught before it propagates into packages/web and packages/api and gets harder to isolate.

It also helps to lean on the monorepo tool's own dependency graph instead of asking Claude Code to infer it from imports alone. Turborepo and Nx both know the real dependency graph; Claude Code reasoning from grep output is a reasonable approximation but not a substitute. A prompt like this closes that gap:

Run `pnpm turbo run build --dry-run --filter=shared...` and show me the
output so we both know which packages actually depend on packages/shared
before I ask you to change anything in it.

The ... filter syntax in Turborepo (or the equivalent nx graph output in Nx) tells you the true consumer set — no guessing, no relying on an agent's mental model of the codebase, which can go stale the moment someone adds a new import.

Custom Subagents for Package-Specific Work

Claude Code supports custom subagents — specialized configurations with their own system prompt and tool access, invoked for a particular kind of task. In a monorepo, subagents are a natural fit for package-specific expertise that you don't want polluting the main conversation's context.

A subagent definition lives in .claude/agents/ as a Markdown file with frontmatter:

---
name: api-test-runner
description: Runs and fixes failing tests in packages/api. Use after any
  change to packages/api or packages/shared that could affect the API.
tools: Read, Edit, Bash, Grep, Glob
---

You are a specialist for the packages/api Fastify backend in this monorepo.

When invoked:
1. Run `pnpm turbo run test --filter=api` and capture the output.
2. For each failing test, read the relevant route handler and the test file.
3. Fix the underlying issue in packages/api or packages/shared — never
   patch a test to make it pass without understanding why it failed.
4. Re-run the test suite and confirm it passes before reporting back.

Do not touch packages/web or packages/mobile. If a failure appears to
originate in one of those packages, report it instead of fixing it.

Once defined, you can invoke this subagent from the main session whenever an edit to packages/shared might have downstream effects on the API, without dragging the full API test output into your main conversation's context window. This is especially valuable in monorepos with four or more packages, where a single session touching everything tends to accumulate enough context that Claude Code's effective attention to any one package degrades. Splitting by package keeps each subagent's context tight and relevant.

Handling Shared Dependencies and Version Drift

A common monorepo failure mode has nothing to do with AI at all: packages/web is on React 18, packages/mobile uses React Native's bundled React version, and packages/shared has peer dependency ranges that technically satisfy both but in practice mask real incompatibilities. When Claude Code proposes a change to packages/shared, it needs to know these constraints exist, or it will "fix" a type error by loosening a peer dependency range in a way that reintroduces the exact problem the range was preventing.

This is worth stating directly in packages/shared/CLAUDE.md:

# packages/shared

## Dependency constraints — read before changing package.json
- `react` peerDependency range must stay `>=18.0.0 <19.0.0` — packages/mobile
  is pinned to React Native's bundled React 18 build and will break on 19.
- `zod` must match the major version pinned in packages/api exactly, because
  packages/api relies on non-guaranteed internal error shapes for validation
  messages.
- Do not add new dependencies to packages/shared without checking whether
  they are already present (possibly at a different version) in the
  consuming packages — check with `pnpm why <package> -r`.

pnpm why <package> -r (or the equivalent yarn why / npm ls <package> -ws) is worth calling out explicitly in your instructions, because it is the fastest way for Claude Code to verify a version constraint claim instead of trusting a comment that might be stale. Encouraging the agent to run the verification command rather than take the CLAUDE.md note as gospel is a good habit generally — documentation drifts, package.json files don't lie.

Managing Context Size as the Repo Grows

Large monorepos eventually hit a practical limit that has nothing to do with configuration correctness: there is simply too much code for any context window to hold at once, and Claude Code has to make choices about what to read. A few habits keep this manageable rather than becoming a source of silently degraded output.

  • Use .claudeignore (or equivalent ignore patterns in your settings) to exclude generated artifacts, lockfiles, and build caches — dist/, .next/, .turbo/, coverage/, and lockfiles like pnpm-lock.yaml rarely need to be read by the agent and only crowd out useful context.
  • Keep package-level CLAUDE.md files short and specific rather than exhaustive. A 400-line CLAUDE.md that tries to document every API route is worse than a 40-line one that documents conventions and points to the code itself for specifics — Claude Code reads the actual source files fine on its own.
  • When a task only needs one package, launch the session from that package's directory rather than the monorepo root. This is the single cheapest context optimization available and it's easy to forget under time pressure.
  • Periodically prune stale instructions. A CLAUDE.md that still describes a testing framework you migrated away from six months ago actively misleads the agent — it is worse than no instruction at all, because it is confidently wrong rather than merely absent.

None of these are exotic techniques. They are the same discipline you'd want from a human engineer joining the team: read the docs that are actually relevant to the task at hand, verify claims against the real dependency graph instead of trusting stale comments, and don't wander into packages you weren't asked to touch.

Practical Checklist for Monorepo Setup

If you're setting this up for the first time, this is roughly the order that pays off fastest:

  1. Write a root CLAUDE.md covering package manager, monorepo tool, and global rules.
  2. Add a package-scoped CLAUDE.md to each package under active development, focused on conventions and constraints rather than exhaustive docs.
  3. Configure .claude/settings.json permission rules so generated directories are denied and sensitive packages require confirmation.
  4. Start sessions from the package directory you're actually working in, not always the repo root.
  5. For cross-package changes, ask Claude Code to survey and report before editing, and lean on your monorepo tool's real dependency graph rather than inferred imports.
  6. Define a custom subagent for any package that has its own test suite and conventions distinct enough to warrant an isolated context.
  7. Revisit and prune CLAUDE.md files whenever a package's tooling changes — stale instructions are actively harmful, not neutral.

None of this is a one-time setup you configure and forget. Monorepos evolve — packages get added, split, deprecated — and the configuration that makes Claude Code effective needs to evolve alongside it. Treat your CLAUDE.md files and .claude/settings.json the same way you'd treat any other piece of shared tooling: reviewed in pull requests, updated when the underlying structure changes, and owned by whoever touches that package most.

Where to Go From Here

Getting Claude Code to behave well in a multi-package repository is less about finding a clever prompt and more about giving it the same onboarding information you'd give a new engineer: where things live, what depends on what, and what's off-limits without asking first. Once that scaffolding exists, the tool stops being a source of scope-creeping diffs and starts being genuinely useful for the kind of cross-cutting work monorepos generate constantly — shared type changes, dependency bumps, and refactors that touch three packages at once.

If you want a structured, hands-on walkthrough of setting up Claude Code from scratch — including workspace configuration, permission rules, and custom subagents like the ones covered here — our Claude Code Tutorial for Beginners course on teachyou.ai walks through exactly this kind of setup step by step, from a single-package project through to a full multi-package workspace.

Claude Code for Monorepos: Managing Multiple Packages · TeachYou Academy