· claude-code / monorepo / turborepo

How to set up Claude Code for a monorepo

A hands-on guide to CLAUDE.md hierarchy, per-package scoping, --add-dir, parallel sessions with worktrees, and the gotchas that will bite you first. Verified on Claude Code v2.1.141, May 2026.

By Ethan

1,658 words · 9 min read

Claude Code works well in monorepos once you understand one non-obvious behavior: subdirectory CLAUDE.md files do not load at startup. They load on demand, when Claude first reads a file in that directory. Get that wrong and you’ll spend a session wondering why your per-package conventions are being ignored.

This guide sets up a pnpm + Turborepo monorepo with Claude Code v2.1.141. Every step is a terminal command you can run today.

Who this is for

Developers who already use Claude Code and are moving an existing project to a monorepo, or starting a new monorepo and want Claude Code wired up correctly from day one. If you haven’t installed Claude Code yet, run npm install -g @anthropic-ai/claude-code and get a session working in a single-package project first.

Why monorepos complicate Claude Code

Four things get harder when you move from a single package to a monorepo:

Context window pressure. Claude walks up from your working directory and loads every CLAUDE.md it finds. In a deep monorepo, that’s root + per-app + per-package rules before you type a single word. Large rule files compound fast.

Path ambiguity with pnpm symlinks. node_modules/@acme/ui is a symlink to packages/ui/. Claude follows it. If you have React loaded twice across symlink boundaries, you’ll see unexpected behavior — modules sharing state they shouldn’t.

Scope defaults. By default, Claude Code can only read and edit files under the current working directory. Running from packages/api/ means Claude can’t see packages/shared/ unless you explicitly extend its reach.

Permission friction. Every new directory Claude tries to access in default mode triggers a prompt. This is fine for one-off exploration; it kills flow on cross-package work.

1. Create the monorepo

pnpm dlx create-turbo@latest my-monorepo
cd my-monorepo

This scaffolds a Turborepo with pnpm workspaces, apps/, and packages/. Verify it builds before touching Claude Code:

pnpm turbo build

2. Write the root CLAUDE.md

The root CLAUDE.md loads every time you run claude from the repo root. Keep it under 200 lines — longer files lose adherence. Put the package manager, build commands, and cross-cutting rules here. Nothing else.

cat > CLAUDE.md << 'EOF'
# Monorepo: @acme

## Package manager
Always use pnpm. Never npm or yarn.
- Install: `pnpm add <pkg> --filter @acme/<package>`
- Build all: `pnpm turbo build`
- Test all: `pnpm turbo test`
- Dev server: `pnpm turbo dev`

## Structure
- apps/       — deployable applications
- packages/   — shared internal libraries
- tooling/    — shared configs (ESLint, TypeScript base)

## Cross-cutting rules
- Internal packages use the `workspace:*` protocol
- Never import from another package's `src/` — import by package name
- No circular dependencies (`pnpm turbo lint` checks this)
- TypeScript strict mode everywhere
- Named exports only
- `@acme/types` is the single source of truth for shared interfaces
EOF

If you’d rather have Claude generate this from the codebase:

CLAUDE_CODE_NEW_INIT=1 claude   # interactive multi-phase /init

The CLAUDE_CODE_NEW_INIT=1 flag enables an interactive multi-phase flow: it explores your codebase with a subagent, asks which artifacts to set up (CLAUDE.md files, skills, hooks), fills in gaps via follow-up questions, and presents a reviewable proposal before writing anything.

What does not belong in root CLAUDE.md: framework-specific conventions (Next.js page router vs. app router, Prisma patterns), personal dev URLs or credentials (those go in CLAUDE.local.md, which is gitignored), and multi-step procedures. Turn those into skills instead.

3. Write per-package CLAUDE.md files

Each package gets its own CLAUDE.md for framework-specific rules. Here’s one for an Express API package:

cat > packages/api/CLAUDE.md << 'EOF'
# @acme/api

Framework: Express.js + TypeScript
Database: Prisma (schema at prisma/schema.prisma)
Validation: Zod

## Commands
Build: pnpm --filter @acme/api build
Test:  pnpm --filter @acme/api test
Dev:   pnpm --filter @acme/api dev

## Conventions
- Routes live in src/routes/
- All inputs validated with Zod
- Async error handler wraps every route handler
- Use @acme/types for shared interfaces — never redefine locally
EOF

The critical behavior to understand: this file does NOT load when you start Claude from the repo root. It loads on demand, the first time Claude reads any file under packages/api/. Running Claude from inside packages/api/ is different — then both the root CLAUDE.md and the package CLAUDE.md load at startup.

For large teams where per-directory files get messy, use path-scoped rules instead. Create .claude/rules/api.md at the repo root:

---
paths:
  - "packages/api/**"
---
# API Package Rules
- All API endpoints must include Zod input validation
- Use the standard error response format from @acme/types

These rules load only when Claude works with matching file paths. Same result, centralized in one location.

4. Choose your working directory

How you start Claude Code changes what loads and what you can access:

From repo rootFrom package dir
Loads at startupRoot CLAUDE.md onlyRoot + package CLAUDE.md
Package CLAUDE.mdOn demandAt startup
Default file accessEverything under repo rootOnly the package dir
Cross-package editsEasy — no extra configNeeds --add-dir
RiskAccidental edits to sibling packagesCan’t see sibling packages without help
Best forCross-package refactors, type changesSingle-package feature work

When you’re working on one package but need access to shared code, extend the scope:

# One-time flag
claude --add-dir ../packages/shared --add-dir ../packages/types

Or persist it in packages/api/.claude/settings.json:

{
  "permissions": {
    "additionalDirectories": ["../packages/shared", "../packages/types"]
  }
}

One important limit: --add-dir grants file access but does NOT scan the added directories for sub-agents or additional CLAUDE.md files. To also load CLAUDE.md from added dirs, set CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1.

5. Reduce permission friction

Pre-approve the paths you know Claude will need. In .claude/settings.json at the repo root:

{
  "permissions": {
    "allow": [
      "Read(./packages/**)",
      "Read(./apps/**)",
      "Edit(./packages/**)",
      "Edit(./apps/**)",
      "Bash(pnpm *)",
      "Bash(turbo *)",
      "Bash(git diff *)",
      "Bash(git status *)"
    ],
    "deny": [
      "Read(./.env*)",
      "Read(./secrets/**)"
    ]
  }
}

Commit this file. Every developer on the team gets the same defaults.

6. Set up parallel sessions with worktrees

For parallel work across packages, git worktrees let you run separate Claude Code sessions on separate branches without context bleeding between them:

git worktree add ../api-feature -b feat/api-auth
git worktree add ../web-feature -b feat/web-auth

# Run a separate Claude session in each
cd ../api-feature && claude
cd ../web-feature && claude

For a large monorepo, add worktree optimizations to .claude/settings.json:

{
  "worktree": {
    "baseRef": "fresh",
    "symlinkDirectories": ["node_modules", ".turbo"],
    "sparsePaths": ["packages/api", "packages/shared", "tooling"]
  }
}

symlinkDirectories symlinks node_modules from the main repo into each worktree — no re-running pnpm install. sparsePaths uses git sparse-checkout cone mode, writing only the listed directories to disk. On a monorepo with 50+ packages, this makes each worktree creation near-instant.

For parallelism inside a single session, create sub-agents under .claude/agents/:

mkdir -p .claude/agents

cat > .claude/agents/api-agent.md << 'EOF'
---
name: api-agent
description: Handles all changes to packages/api. Use when modifying API routes, Prisma schema, or Express middleware.
tools: [Read, Edit, Bash, Grep, Glob]
model: sonnet
---
You work exclusively in packages/api. Never modify files outside this directory.
Framework: Express.js + TypeScript. Always run `pnpm --filter @acme/api test` after changes.
EOF

Claude’s lead session routes tasks to the right sub-agent based on the description. Each sub-agent has its own context window.

7. Gotchas

pnpm symlinks follow-through. node_modules/@acme/uipackages/ui/. Claude will follow symlinks and may suggest edits inside packages/ui/ when you’re working in apps/web/. After renaming a package, stale symlinks produce ENOENT errors — fix with pnpm install --force.

MCP servers fail in strict pnpm repos (known issue; #38275 closed as duplicate — check parent for current status). Claude Code launches built-in MCP servers via npx from the project directory. If your .npmrc requires pnpm as the package manager, npx fails silently and MCP tools disappear. Tracked in anthropics/claude-code #38275. Workaround: configure MCP server launch explicitly with pnpm dlx.

Turbo cache stale reads. .turbo/ caches build outputs. If the cache is stale, Claude reads outdated dist/ files. Add .turbo/ to .gitignore (Turborepo does this by default — double-check it’s there) and set "cache": false for test tasks in turbo.json.

claudeMdExcludes for team isolation. In monorepos with multiple teams, Claude picks up all ancestor CLAUDE.md files. Use this setting to block irrelevant ones:

{
  "claudeMdExcludes": ["**/packages/legacy-team/**/.claude/rules/**"]
}

worktree.baseRef changed in v2.1.133. Before this version, EnterWorktree branched from local HEAD. After v2.1.133, it branches from origin/<default> (called fresh mode). If you have unpushed commits you need in a new worktree, set:

{ "worktree": { "baseRef": "head" } }

Quick-start file structure

my-monorepo/
├── CLAUDE.md                          ← package manager, structure, cross-cutting rules
├── CLAUDE.local.md                    ← gitignored: personal URLs, dev preferences
├── .claude/
│   ├── settings.json                  ← committed: permissions, additionalDirectories
│   ├── settings.local.json            ← gitignored: personal overrides
│   ├── agents/
│   │   ├── api-agent.md               ← sub-agent for packages/api
│   │   └── web-agent.md               ← sub-agent for apps/web
│   └── rules/
│       ├── api.md                     ← paths: ["packages/api/**"]
│       └── web.md                     ← paths: ["apps/web/**"]
├── pnpm-workspace.yaml
├── turbo.json
├── apps/
│   └── web/
│       └── CLAUDE.md                  ← Next.js conventions, test commands
└── packages/
    ├── types/
    │   └── CLAUDE.md                  ← "breaking changes affect all dependents"
    ├── api/
    │   └── CLAUDE.md                  ← Express/Prisma conventions
    └── shared/
        └── CLAUDE.md

.gitignore additions:

CLAUDE.local.md
.claude/settings.local.json
.turbo/
*.local

To auto-generate per-package CLAUDE.md files from your workspace definition:

pip install claude-init-monorepo
claude-init-monorepo   # reads pnpm-workspace.yaml, runs /init per package

References