← All guides

Claude Code in a Monorepo: Workspace-Aware CLAUDE.md, Scoped Edits, Cost Control (2026)

Practical Claude Code patterns for Turborepo, pnpm, Nx, and Bun workspaces — per-package CLAUDE.md, scoped edits, parallel agent runs, cost-per-package tracking.

Claude Code works in monorepos, but it does not scale by default. To make it usable across a Turborepo, pnpm, Nx, or Bun workspace you need three things: per-package CLAUDE.md files so context is isolated to the workspace you are touching, scoped edits via cd packages/<pkg> && claude (or directory-locked subagents) so the model cannot accidentally edit a sibling package, and cost tracking by workspace using the Anthropic Admin API's workspace_id field. This guide walks through all three patterns with Turborepo + pnpm examples, plus Nx and Bun notes, and the five pitfalls we have repeatedly seen teams hit in 2026.

Why monorepos break the default Claude Code workflow

A single root-level CLAUDE.md is the default Claude Code recommendation, and it works fine for repos under ~50 files. In a real monorepo it fails three ways at once:

  1. Context bloat. Every session loads the full root CLAUDE.md. Documenting apps/web, apps/api, packages/ui, packages/db, packages/types in one file spends 3-8k tokens on context the model does not need before the first prompt arrives.
  2. Wrong-package edits. With the whole repo in scope, "add a debounce hook" can land in packages/ui when you meant apps/web/src/hooks. We have seen monorepos accumulate duplicate utilities — one per app — because Claude could not tell which package "owned" a given concern.
  3. Untraceable cost. Every Claude API call hits the same workspace. No way to answer "how much did apps/api cost us this month?" without manual tagging.

The fix is not "split the monorepo." It is to teach Claude Code about workspace boundaries the way Turborepo, pnpm, Nx, and Bun teach the build system.

Pattern 1 — Per-package CLAUDE.md hierarchy

Claude Code merges CLAUDE.md files up the directory tree. If you launch a session inside packages/ui, it reads packages/ui/CLAUDE.md first, then walks up and merges the root CLAUDE.md on top. This means you can keep the root file small (repo-wide conventions only) and push package-specific context down to where it belongs.

A working layout for a Turborepo:

my-monorepo/
├── CLAUDE.md                      # repo-wide: pnpm, turbo, commit style
├── apps/
│   ├── web/CLAUDE.md              # Next.js 15, app router, Tailwind v4
│   └── api/CLAUDE.md              # Hono + Drizzle, auth conventions
├── packages/
│   ├── ui/CLAUDE.md               # shadcn fork, theming rules
│   ├── db/CLAUDE.md               # schema migration policy
│   └── types/CLAUDE.md            # zero deps, types-only
└── turbo.json

A good per-package CLAUDE.md is 30-80 lines, not 300:

# packages/db

This package owns the Postgres schema and Drizzle migrations.

## Rules
- Never import from `apps/*`. This package is leaf-level.
- Migrations are append-only. To change a column, write a new migration.
- Run `pnpm db:generate` after editing `schema.ts`. Commit both.
- Tests use `pglite` in-memory; do not require a running DB.

## Files
- `src/schema.ts` — single source of truth for all tables
- `src/migrations/` — generated, do not hand-edit
- `src/seed.ts` — dev-only fixtures

## Common tasks
- Adding a column: edit `schema.ts` → `pnpm db:generate` → review SQL diff
- Renaming a column: two migrations (add new, backfill, drop old)

The root CLAUDE.md then collapses to ~20 lines: monorepo tool, package manager, commit convention, links to per-package files. Token spend drops by 60-80% on package-scoped sessions. Per-package files should cover boundary rules (what this package can import), generated files (what not to hand-edit), three to five common task recipes, and the single test command Claude should run after a change — pnpm --filter ./packages/ui test, not pnpm test.

Pattern 2 — Scoped sessions

Once per-package files exist, start the session inside the package:

cd packages/ui && claude

This does two things. First, it makes packages/ui/CLAUDE.md the primary context. Second, every relative path Claude resolves is rooted at the package, so Edit src/Button.tsx cannot wander into apps/web by accident.

For teams that want a single launcher, a thin wrapper script:

#!/usr/bin/env bash
# scripts/claude-pkg
# Usage: claude-pkg ui   →  cd packages/ui && claude
set -euo pipefail
pkg="${1:?package name required}"
root="$(git rev-parse --show-toplevel)"

if   [[ -d "$root/packages/$pkg" ]]; then target="$root/packages/$pkg"
elif [[ -d "$root/apps/$pkg"     ]]; then target="$root/apps/$pkg"
else echo "no such workspace: $pkg" >&2; exit 1
fi

cd "$target" && exec claude "$@"

Avoid the symlink trick (ln -s ../../CLAUDE.md ./CLAUDE.md) some teams use to "share" context — it defeats the whole point of per-package files. If two packages genuinely share rules, put them in the root CLAUDE.md; the model walks up the tree on its own.

A subtle benefit of scoped sessions is better diff hygiene — commit messages naturally scope themselves to the package (feat(ui): add useDebounce hook rather than generic feat: add hook), which pairs well with conventional commits and Changesets-style release tooling.

Pattern 3 — Workspace-aware subagents

When you genuinely need cross-package work (e.g. "add a useDebounce hook to packages/ui and consume it in apps/web"), spawn a subagent scoped to one package rather than letting the parent edit both. Configure your subagent in .claude/agents/ui-only.md:

---
name: ui-only
description: Edits packages/ui only. Use for component, hook, and theme work.
tools: Read, Edit, Write, Bash(pnpm test:*)
---

You are scoped to `packages/ui`. Do not read or edit files outside this directory.
If a change requires touching another package, return a description of the
required change and let the parent agent handle it.

The parent reads packages/ui output, then makes the apps/web edit itself. This keeps each agent's context small and gives you a clean per-package audit trail. For genuinely independent work — say, adding the same feature flag config to four leaf packages — spawn four scoped agents in parallel: smaller diffs, lower total cost, and no cross-package context loading.

Turborepo specifics

Turborepo's strength is its task graph. You can wire Claude into it like any other task — useful for "draft a changeset" or "regenerate this README" steps:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "claude:docs": {
      "cache": false,
      "inputs": ["src/**", "CLAUDE.md"],
      "outputs": ["README.md"]
    },
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }
  }
}

Two cache caveats. Never cache Claude outputs as build artifacts — Anthropic's models are non-deterministic, so a cache hit would replay stale generation. Mark Claude tasks "cache": false. Do include CLAUDE.md in inputs for any task that reads it, so a context change invalidates downstream caches that consume the generated artifact.

pnpm workspaces

pnpm's hoisting means a lot of dev dependencies live at the root, not the package. When Claude is asked "why is vitest available here?" inside packages/ui, the answer is in pnpm-lock.yaml at the repo root. Two practical rules:

Nx workspaces

Nx ships a project graph (nx graph) that maps every project's dependencies. You can hand Claude the subgraph instead of the full repo:

nx graph --file=graph.json --focus=web

That JSON lists only web and its transitive deps. Pipe it into the session as a one-time context dump and you get the same scoping effect as a per-package CLAUDE.md, but derived from the build graph instead of hand-written. For Nx repos with 50+ projects this is the cleanest pattern we have found.

Bun workspaces

Bun 1.x workspaces are the simplest of the four — package.json with a workspaces array, no extra config. The flip side: no equivalent of nx graph or turbo run --filter, so you fall back to per-package CLAUDE.md plus cd packages/<pkg> && claude. One Bun-specific tip: bun.lockb is binary and unreadable by the model, so if your context depends on dependency versions, export with bun pm ls --all > .deps.txt and reference that file from CLAUDE.md.

Cost Optimization Masterclass — the per-workspace cost-tracking patterns in the next section come from a 2-hour deep dive on monorepo-scale Claude spend, including the dashboard queries we use to keep apps/api under $40/month. Get the masterclass.

Cost control per workspace

Anthropic's Admin API exposes a workspace_id on every API request and on the usage and cost reports. In a monorepo you have two good options:

  1. One Anthropic workspace per package. Heavier setup, but the cleanest reporting — you get cost-per-package directly out of the Admin API with no tagging.
  2. One workspace, custom header tagging. Use a single workspace_id and tag each request with a x-package header (or pass it as a metadata field in the SDK), then aggregate downstream.

A minimal Admin API filter for option 1:

curl https://api.anthropic.com/v1/organizations/usage_report/messages \
  -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \
  -H "anthropic-version: 2023-06-01" \
  --data-urlencode "starting_at=2026-05-01T00:00:00Z" \
  --data-urlencode "ending_at=2026-05-09T00:00:00Z" \
  --data-urlencode "group_by[]=workspace_id" \
  --data-urlencode "group_by[]=model"

The response groups token spend by workspace, so apps_api and packages_ui show up as separate rows. For deeper coverage of the Admin API endpoints — including budget alerts and per-workspace rate limits — see our Anthropic Admin API tutorial. Aggregate the JSON in claudecosts.app, a self-hosted Grafana board, or even a daily Google Sheet pull. Tag the report with the package name in your dashboard and you can finally answer "is apps/web worth what we are spending on it?"

For option 2 (single workspace, header tagging), the SDK call looks like this:

import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();

await client.messages.create({
  model: "claude-sonnet-4-7",
  max_tokens: 1024,
  metadata: { user_id: `pkg:${process.env.PACKAGE_NAME}` },
  messages: [{ role: "user", content: "..." }],
});

Set PACKAGE_NAME in each package's .env (or read from package.json#name) and your usage report can group by metadata.user_id prefix. This is lighter weight than spinning up multiple workspaces, but the reporting is only as good as your tagging discipline — one untagged call and the cost lands in "unknown."

Common pitfalls (5)

  1. Root CLAUDE.md too large. Over 100 lines means it is doing per-package work. Litmus test: if a section starts with "in packages/foo," it belongs in packages/foo/CLAUDE.md.
  2. Cross-package edits Claude misses. A type change in packages/types rippling to four consumers is the most common silent failure — types tests pass, four consumers break. Mitigate with a pre-commit hook running tsc --noEmit repo-wide, or a turbo run typecheck step. See our Claude Code hooks deep dive for hook patterns.
  3. Unattended Claude Code in CI/CD. Never give a CI runner full repo write permission — no human in the loop to catch a misfire. Use the Agent SDK with an explicit tool list and path deny-list, or scope to a single package via working directory.
  4. Stale CLAUDE.md. Renames invalidate every CLAUDE.md mentioning the old path. Add a CI check that greps each CLAUDE.md for paths that no longer exist. Stale guidance is worse than none — the model follows it confidently.
  5. Subagent context bleed. A scoped subagent still sees the parent's conversation history unless you set the system prompt to ignore it and pass an explicit working directory at spawn time. Verify by asking the subagent "what files are you allowed to edit?"

For the foundations of Claude Code itself — slash commands, permissions, model routing — start with our complete Claude Code guide.

Frequently Asked Questions

Should every package have its own CLAUDE.md?

No. Apply the rule "if a package needs more than two sentences of context, give it a CLAUDE.md." Leaf packages with three files and zero special rules can rely on the root file. We typically see 60-70% of packages get their own file in production monorepos.

How do I prevent Claude from editing other packages?

Three layers, weakest to strongest. (1) Tell it not to in the per-package CLAUDE.md — works most of the time. (2) Use a directory-scoped subagent — the agent definition restricts the working directory. (3) Add a pre-commit hook that rejects diffs touching files outside the package the session was launched in. Layer 3 is the only one that gives you a hard guarantee.

Cost tracking by package?

Easiest path: one Anthropic workspace per top-level package, then use the Admin API usage_report/messages endpoint with group_by=workspace_id. If you cannot create that many workspaces, tag each request with a metadata.package field via the SDK and aggregate in your own dashboard. Both approaches are documented in the Admin API tutorial linked above.

Does Turborepo cache Claude outputs?

It can, but it should not. Claude responses are non-deterministic — the same inputs hash will produce different outputs across runs, so a cache hit would silently replay stale generation. Always set "cache": false on tasks that invoke Claude. Turbo can still cache the consumers of those outputs (e.g. the build task that reads a Claude-generated file), and inputs should include both the source files and the generated artifact so changes invalidate correctly.

What about microservices repos?

A microservices repo is a monorepo with deployment boundaries — every pattern here applies. Extra rule: each service's CLAUDE.md should forbid editing deployment manifests (Dockerfile, fly.toml, k8s YAML) without a human in the loop. A "helpful" tweak to a memory limit can take prod down. Keep manifests in a single infra/ directory marked read-only, and pair with a hook that rejects writes from non-infra sessions.


Last verified May 2026 against Turborepo 2.x, pnpm 9, Nx 19, and Bun 1.x. Patterns drawn from monorepos in the 5-30 package range; very large repos (100+ packages) need additional graph-based context selection that we will cover in a follow-up.

AI Disclosure: Drafted with Claude Code; tested against Turborepo 2.x, pnpm 9, Nx 19, and Bun 1.x workspace setups May 2026.

Tools and references