Originally published at claudeguide.io/claude-code-monorepo-guide
**Claude Code in monorepos needs 3 patterns to scale beyond ~50 files: per-package CLAUDE.md (isolates context), scoped edits via cd packages/<pkg
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:
- 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.
-
One workspace, custom header tagging. Use a single
workspace_idand tag each request with ax-packageheader (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-6",
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)
-
Root
CLAUDE.mdtoo large. Over 100 lines means it is doing per-package work. Litmus test: if a section starts with "inpackages/foo," it belongs inpackages/foo/CLAUDE.md. -
Cross-package edits Claude misses. A type change in
packages/typesrippling to four consumers is the most common silent failure —typestests pass, four consumers break. Mitigate with a pre-commit hook runningtsc --noEmitrepo-wide, or aturbo run typecheckstep. See our Claude Code hooks deep dive for hook patterns. - 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.
-
Stale
CLAUDE.md. Renames invalidate everyCLAUDE.mdmentioning the old path. Add a CI check that greps eachCLAUDE.mdfor paths that no longer exist. Stale guidance is worse than none — the model follows it confidently. - 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.
Claude API Cost Optimization Masterclass ($59) — Per-workspace cost control is one chapter. The full playbook covers prompt caching, model tiering, Batch API routing, and the spreadsheet framework to forecast and cap monthly Claude spend before it surprises you.
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.
Top comments (0)