Setting Up Claude Code for Monorepos: Multi-Package Context Management

Nicola·
Setting Up Claude Code for Monorepos: Multi-Package Context Management

Setting Up Claude Code for Monorepos: Multi-Package Context Management

Monorepos are where AI coding agents go to die. A single repository with 5 packages, shared libraries, and cross-package imports creates a context management nightmare that turns Claude Code from a productivity multiplier into a token-burning confusion machine.

The symptoms are unmistakable: Claude Code suggests importing from `@org/shared-utils` but writes the path as a relative import. It reads the wrong package's `tsconfig.json`. It creates a React component using patterns from the Express API package that lives three directories over. Every fix creates two new problems because the agent doesn't understand package boundaries.

Here's how to configure Claude Code for monorepos so it actually works — and how to manage context across packages without burning your entire token budget on irrelevant code.

Why Monorepos Confuse AI Agents

The root problem is structural ambiguity. In a single-package project, the file tree is the architecture. In a monorepo, the file tree is misleading — packages that sit next to each other in the directory may have completely different runtimes, dependencies, and coding patterns.

Consider a typical monorepo:

```

packages/

web/ → Next.js frontend (React, Tailwind)

api/ → Express backend (Node.js, Prisma)

shared/ → Shared types and utilities (pure TS)

mobile/ → React Native app

workers/ → Cloudflare Workers (edge runtime)

```

When you ask Claude Code to "add input validation to the user form," it needs to know:

  • The form is in `packages/web/`
  • The validation schema might live in `packages/shared/`
  • The API endpoint it submits to is in `packages/api/`
  • The validation library (Zod) is a dependency of `shared`, not `web`
  • Import paths use `@org/shared` workspace aliases, not relative paths

Without explicit guidance, Claude Code has a 50/50 chance of reading from the wrong package. It might find a validation utility in `packages/api/src/utils/validate.ts` and suggest importing it into the web frontend — an import that will compile but crash at runtime because the API package uses Node.js APIs unavailable in the browser.

The Three Core Problems

Problem 1: Wrong-package reads. Claude Code explores by proximity in the file tree. When it reads `packages/web/src/components/UserForm.tsx` and needs to understand the validation logic, it may scan `packages/api/src/` before checking `packages/shared/` because the API package has more files and appears more relevant based on filename matching.

Problem 2: Import confusion. Monorepos use workspace aliases (`@org/shared`), path mappings (`tsconfig.json` paths), or package.json `exports` fields. Claude Code defaults to relative imports unless told otherwise, producing import statements that look correct but fail resolution.

Problem 3: Pattern contamination. Each package may follow different patterns. The API uses service classes and dependency injection. The web app uses React hooks and functional components. The workers use a minimal handler pattern. Claude Code picks up patterns from whatever files it reads first, regardless of which package it's actually working in.

The Monorepo Setup Checklist

Fix these configuration issues before writing a single prompt, and 80% of monorepo-related Claude Code problems disappear.

1. Root CLAUDE.md — The Map

Create a `CLAUDE.md` at the repository root that describes the monorepo structure:

```markdown

Monorepo Structure

Packages

  • `packages/web` — Next.js frontend (React 19, Tailwind v4)
  • `packages/api` — Express API server (Node.js 22, Prisma)
  • `packages/shared` — Shared types, validation schemas, utilities
  • `packages/mobile` — React Native app (Expo)
  • `packages/workers` — Cloudflare Workers (edge runtime)

Import Rules

  • Cross-package imports MUST use workspace aliases: @org/shared, @org/api
  • NEVER use relative paths across package boundaries
  • packages/shared is the ONLY package that can be imported by other packages
  • packages/api and packages/web MUST NOT import from each other

Package Manager

  • pnpm with workspaces
  • Run commands from package directories: cd packages/web && pnpm dev

```

This file costs ~200 tokens to read and prevents thousands of tokens of wrong-package exploration.

2. Per-Package CLAUDE.md — The Conventions

Each package gets its own `CLAUDE.md` with package-specific conventions:

```markdown

packages/web

Stack

  • Next.js 15 App Router
  • React 19 with Server Components
  • Tailwind CSS v4
  • Zod (via @org/shared) for validation

Commands

  • `pnpm dev` — start dev server (port 3000)
  • `pnpm test` — vitest
  • `pnpm lint` — eslint + prettier

Patterns

  • Use Server Components by default, 'use client' only when needed
  • Co-locate tests in __tests__/ directories
  • Import validation schemas from @org/shared/validation
  • State management: Zustand for client state, React Query for server state

```

When Claude Code works in `packages/web/`, it reads both the root and package-level `CLAUDE.md`. The per-package file overrides general patterns with package-specific ones.

3. Workspace Configuration

Ensure your workspace tooling is properly configured so Claude Code's commands work correctly:

`package.json` (root):

```json

{

"workspaces": ["packages/*"]

}

```

`tsconfig.json` (root):

```json

{

"compilerOptions": {

"paths": {

"@org/shared/*": ["packages/shared/src/*"],

"@org/api/*": ["packages/api/src/*"]

}

}

}

```

Claude Code reads these files to understand import resolution. If your workspace aliases aren't defined in `tsconfig.json`, Claude Code will fall back to guessing import paths — and it will guess wrong.

Context Management Strategies

Even with proper setup, monorepo context management requires discipline. The goal: keep Claude Code focused on one package at a time, with minimal cross-package context.

Strategy 1: Scope Every Prompt

Never say "add validation." Always say "add validation to `packages/web/src/components/UserForm.tsx` using the schema from `@org/shared/validation/user.ts`."

Every prompt should include:

  • Which package you're working in
  • Which files are relevant
  • Which cross-package imports are needed (with exact paths)

This isn't pedantic — it's token-efficient. A scoped prompt costs 50 tokens. An unscoped prompt triggers 5,000+ tokens of exploration as Claude Code figures out which package you mean.

Strategy 2: Use /clear When Switching Packages

If you've been working in `packages/web/` and need to switch to `packages/api/`, use `/clear` first. The web package context (component patterns, React hooks, Tailwind classes) will contaminate Claude Code's reasoning about the API package. A clean context window produces better results than a polluted one.

Cost of clearing: You lose accumulated context from the previous package (5-10 minutes to rebuild).

Cost of NOT clearing: Pattern contamination that produces wrong-package suggestions (15-30 minutes to debug and fix).

The math always favors clearing.

Strategy 3: Reference Shared Libraries Explicitly

When your task involves shared code, tell Claude Code exactly what to reference:

```

I'm working in packages/web. For this task, you'll need:

  • The UserProfile type from @org/shared/types/user.ts
  • The validateEmail schema from @org/shared/validation/user.ts

Do NOT read any other files in packages/shared unless needed.

```

This surgical context loading prevents Claude Code from reading the entire shared package (~15,000 tokens) when it only needs two type definitions (~200 tokens).

Strategy 4: Per-Package Terminal Sessions

For long monorepo work sessions, run separate Claude Code instances per package. Each instance stays scoped to its package's context, conventions, and patterns. When you need cross-package coordination, pass specific information between sessions manually.

This approach mirrors how human developers work in monorepos — you don't keep the entire monorepo in your head. You focus on one package, check the shared types when needed, and switch context deliberately.

How vexp Handles Monorepos

Context engines designed for monorepos solve the cross-package problem at the index level. vexp, for example, supports multi-repo indexing — each package gets its own dependency graph, and cross-package imports are tracked as inter-graph edges.

When you ask vexp for context in a monorepo, it does three things that manual exploration can't:

1. Package-aware scoping. It knows that `packages/web/src/components/UserForm.tsx` depends on `@org/shared/validation/user.ts` but NOT on `packages/api/src/routes/user.ts`. It serves the shared types without the API package.

2. Cross-package dependency tracking. If you're refactoring a shared type, vexp's impact graph shows which files in EVERY package are affected — not just the package you're working in. Manual search would require grepping each package separately and understanding each one's import resolution rules.

3. Workspace-aware import paths. Context returned by vexp includes the correct workspace alias paths (`@org/shared/validation`) rather than relative paths that break cross-package boundaries.

The setup is straightforward:

```bash

cd monorepo-root

vexp init # initializes at root level

vexp index # indexes all packages

```

vexp detects the workspace configuration from your `package.json` workspaces field or `pnpm-workspace.yaml` and indexes each package as a separate graph with cross-package edges. When Claude Code queries `run_pipeline` from any package, it receives context that respects package boundaries automatically.

Token savings in monorepos are even larger than single-package projects. In a single-package project, wrong-file reads waste maybe 500-2,000 tokens each. In a monorepo, wrong-package reads waste 5,000-15,000 tokens because Claude Code reads multiple files in the wrong package before realizing its mistake. A context engine that prevents wrong-package reads saves 60-75% of exploration tokens in monorepo workflows.

Practical Monorepo Workflow

Here's a real-world workflow for adding a feature that touches multiple packages: adding email verification to the user signup flow.

Step 1: Plan in the Root Context

```

I'm adding email verification to user signup. This will touch:

  • packages/shared (new email verification types and schemas)
  • packages/api (new verification endpoint and email sending)
  • packages/web (verification status UI and redirect flow)

Plan the changes needed in each package and the order of implementation.

```

Claude Code produces an implementation plan. Key insight: changes should go shared → api → web because each package depends on the previous one.

Step 2: Implement Shared Types First

```

Working in packages/shared. Add:

  • VerificationStatus type to src/types/user.ts
  • emailVerificationSchema to src/validation/user.ts
  • verifyEmailToken utility to src/utils/email.ts

```

Clear context after this step.

Step 3: Implement API Changes

```

Working in packages/api. Add an email verification endpoint:

  • POST /api/verify-email/:token
  • Uses VerificationStatus from @org/shared/types/user
  • Uses emailVerificationSchema from @org/shared/validation/user
  • Updates user record via Prisma

Follow existing endpoint patterns in src/routes/auth.ts

```

Clear context after this step.

Step 4: Implement Web Changes

```

Working in packages/web. Add email verification UI:

  • Verification pending banner in src/components/auth/VerificationBanner.tsx
  • Token verification page at src/app/verify-email/[token]/page.tsx
  • Uses VerificationStatus from @org/shared/types/user

Follow existing page patterns and use the project's UI components.

```

Step 5: Integration Test

```

Run tests across all packages:

  • cd packages/shared && pnpm test
  • cd packages/api && pnpm test
  • cd packages/web && pnpm test

Report any failures.

```

Total tokens across all steps: ~40,000-60,000. An unscoped approach ("add email verification to the signup flow" without package specification) would cost 120,000-180,000 tokens as Claude Code explores all five packages trying to understand the monorepo structure.

Common Monorepo Pitfalls

Pitfall 1: Forgetting to clear context between packages. Patterns from `packages/api` (Express middleware, database queries) leak into Claude Code's suggestions for `packages/web` (React components, client-side state). Always clear.

Pitfall 2: Letting Claude Code discover package structure. Don't ask "what packages are in this monorepo?" — tell it. The `CLAUDE.md` at the root should document every package so Claude Code never needs to run `ls packages/` and then read each package.json.

Pitfall 3: Cross-package barrel exports. If `packages/shared/src/index.ts` re-exports everything from 20 submodules, Claude Code reads the entire barrel file (and potentially all 20 submodules) when it only needs one type. Reference specific submodule paths in your prompts: `@org/shared/types/user` instead of `@org/shared`.

Pitfall 4: Inconsistent naming across packages. If the API calls it `userId` and the web app calls it `user_id` and shared types call it `id`, Claude Code picks whichever convention it saw most recently. Standardize naming in shared types and document the convention in CLAUDE.md.

The pattern is consistent: explicit configuration and scoped prompts beat hoping Claude Code figures out your monorepo structure on its own. The 30 minutes you spend on CLAUDE.md files and workspace configuration save hours of debugging wrong-package suggestions.

Frequently Asked Questions

How do I set up Claude Code for a monorepo?
Create a root-level CLAUDE.md that maps the monorepo structure (package names, locations, and purposes), import rules (workspace aliases, cross-package restrictions), and the package manager. Then create per-package CLAUDE.md files with package-specific conventions, commands, and patterns. Ensure your workspace configuration (tsconfig paths, package.json workspaces) is correct so Claude Code can resolve cross-package imports.
Why does Claude Code suggest wrong imports in monorepos?
Claude Code defaults to relative imports and explores files by proximity in the file tree, not by package boundary. It doesn't automatically understand workspace aliases (@org/shared) or which packages can import from which. Fix this by documenting import rules in CLAUDE.md, specifying exact import paths in prompts, and ensuring tsconfig.json paths are configured for workspace aliases.
Should I use one Claude Code session or multiple for monorepo work?
Use /clear between packages at minimum. For long sessions, consider separate Claude Code instances per package. Each instance stays scoped to its package's context, preventing pattern contamination. When changes span packages, implement in dependency order (shared libs first, then consumers) and pass specific type definitions between sessions rather than carrying accumulated context.
How does vexp handle cross-package dependencies in monorepos?
vexp indexes each package as a separate dependency graph with cross-package imports tracked as inter-graph edges. When you query context for a file, vexp returns dependencies that respect package boundaries — it serves the shared types your file needs without pulling in unrelated packages. This prevents wrong-package reads that waste 5,000-15,000 tokens each and reduces total exploration overhead by 60-75% compared to manual search.
What's the most common monorepo mistake with Claude Code?
Not clearing context when switching between packages. Accumulated context from one package (its patterns, imports, naming conventions) contaminates Claude Code's suggestions for another package. This produces code that looks correct but uses wrong patterns, wrong imports, or wrong runtime APIs. Always use /clear when switching packages, and scope every prompt to a specific package with explicit file paths and import references.

Nicola

Developer and creator of vexp — a context engine for AI coding agents. I build tools that make AI coding assistants faster, cheaper, and actually useful on real codebases.

Related Articles