monorepo Claude Code CLAUDE.md AGENTS.md Nx Turborepo Bazel pnpm workspaces token budget CI/CD developer tools 2026

Monorepo × AI Tools: Patterns for 50+ Packages (2026)

The Prompt Shelf ·

Running AI coding tools in a small monorepo is a configuration problem. Running them across 50+ packages is an architecture problem.

The difference matters. Once you cross the 30–50 package threshold, the naive approaches — one root CLAUDE.md, one AGENTS.md, one shared config — start producing measurably worse AI output. The context gets diluted, the token budget balloons, and the AI starts confidently applying the wrong conventions to the wrong packages. You can feel it in the code quality, even if you can’t immediately name the cause.

This guide is the deep-dive follow-up to the Claude Code monorepo setup guide and the AGENTS.md in monorepos guide. Those cover fundamentals. This one covers the patterns that matter when your repo has grown past the point where fundamentals are enough.

We’ll work through seven configuration patterns with concrete tradeoffs, walk through Nx, Turborepo, pnpm workspaces, and Bazel specifically, cover token budget measurement techniques, and address CI/CD integration — the place where most teams discover their configuration is subtly wrong.

Why Scale Changes Everything

Before getting into patterns, it’s worth being specific about what breaks at scale.

The Context Dilution Problem

Claude Code’s context window is large but not infinite, and it’s shared across everything loaded at session start: your CLAUDE.md files, conversation history, file reads, tool outputs. In a small monorepo, even a generous CLAUDE.md setup might consume 15,000–20,000 tokens of context, leaving the rest for actual work.

In a 50+ package repo, naively stacking CLAUDE.md files can consume 60,000–80,000 tokens before a single tool call. That’s not just expensive — it means the actual codebase context gets squeezed out by configuration instructions. The AI becomes very confident about your conventions and less capable of actually reading the relevant code.

The Maintenance Drift Problem

Small monorepos can get away with manual config updates. Large ones can’t. When you have 50 packages and you rename your test runner, you’re updating 50 AGENTS.md files. Half of them will be wrong within a month because the update didn’t propagate cleanly, someone forgot to run the script, or a new package was added without following the template.

Configuration drift at this scale is invisible until it’s wrong — and it’s usually wrong in the subtlest possible way: the AI applies slightly outdated conventions, the code reviews start catching small inconsistencies, nobody can immediately explain why the new package looks different from the old ones.

The Workspace-Tool Mismatch Problem

Nx, Turborepo, pnpm workspaces, and Bazel have meaningfully different execution models, and AI tools behave differently depending on which one you use. A Turborepo-specific AGENTS.md pattern that works cleanly in a 20-package repo produces confusing output in an Nx workspace because the dependency graph resolution is different, the task cache behavior is different, and the “correct” command to run a build in one package looks different depending on whether you want to trigger the dependency chain or not.

The 7 Configuration Patterns

These aren’t exclusive — real repos often combine two or three. What follows is a structured catalog of each pattern, when to use it, and where it breaks.

Pattern 1: Single Root Config

Structure:

monorepo/
└── CLAUDE.md (or AGENTS.md)   # Everything in one file

When it works: Under 15 packages, same tech stack throughout, small team.

When it breaks: Past 20 packages, mixed stacks (e.g., Python services next to TypeScript frontend next to Rust CLI), or whenever the root file exceeds ~500 lines.

Token cost profile: Fixed overhead per session, predictable but not scalable.

# CLAUDE.md — Root (Single Config Pattern)

## Stack
All packages: TypeScript, Node.js 22, pnpm workspaces.

## Build
pnpm build — builds all packages in dependency order (via Turborepo)

## Test
pnpm test — runs Vitest across all packages

## Rules
- No raw SQL. Use the ORM (Prisma in packages/db, Drizzle everywhere else).
- All packages use named exports. No default exports.
- Tests are required. No untested public API surface.

Verdict: Fine to start here. Plan your migration path before you hit 20 packages, not after.


Pattern 2: Per-Package Config (Flat)

Structure:

monorepo/
├── CLAUDE.md                  # Root: cross-cutting only
├── packages/
│   ├── api/CLAUDE.md          # Package-specific
│   ├── web/CLAUDE.md
│   ├── mobile/CLAUDE.md
│   └── [48 more]/CLAUDE.md

When it works: 15–40 packages, relatively independent teams per package, clear ownership boundaries.

When it breaks: Maintenance at 50+ packages becomes expensive. Changes to cross-cutting rules require touching every file. New package templates drift from established ones.

Token cost profile: Variable. The agent only loads the root + the package it’s currently operating in, so per-session cost is bounded regardless of total package count.

# Check how many tokens your current hierarchy loads
# Run from the package directory you're working in
find . -maxdepth 1 -name "CLAUDE.md" -print -exec wc -w {} \;
cd ../.. && find . -maxdepth 1 -name "CLAUDE.md" -print -exec wc -w {} \;

Verdict: The most commonly recommended pattern. Works well until the maintenance burden of 50+ independent files creates more problems than it solves.


Pattern 3: Hierarchical Inheritance

Structure:

monorepo/
├── CLAUDE.md                  # Tier 1: Universal
├── packages/
│   ├── CLAUDE.md              # Tier 2: Package-category defaults
│   ├── api/CLAUDE.md          # Tier 3: Package-specific overrides
│   ├── web/CLAUDE.md
│   └── cli/CLAUDE.md
├── apps/
│   ├── CLAUDE.md              # Tier 2: Apps defaults
│   └── dashboard/CLAUDE.md   # Tier 3: App-specific
└── services/
    ├── CLAUDE.md              # Tier 2: Service defaults
    └── auth-service/CLAUDE.md # Tier 3: Service-specific

When it works: 40–100 packages, multiple distinct categories (apps, packages, services), teams organized by category rather than package.

When it breaks: When category boundaries are fuzzy, or when packages need to override category defaults frequently (many overrides signal the category boundary is wrong).

Token cost profile: Three levels loaded per session: root (~200 words) + category (~150 words) + package (~200 words) = ~550 words / ~750 tokens. Very manageable.

The key design principle: each tier should have zero duplication with the tier above it. The category-level CLAUDE.md should only contain what applies to all packages in that category but not to packages in other categories.

# packages/CLAUDE.md — Category tier (libraries only)

## Library Package Rules
- All public APIs must be documented with JSDoc
- Packages target Node.js 18+, not browser
- No framework dependencies (React, Express, etc.) — utils only
- Bundle with tsup: `pnpm build`
- Publish configuration in package.json `exports` field

## Testing (library packages)
- Vitest for unit tests
- 100% branch coverage on public APIs
- Test with Node.js 18 and 22 (matrix in CI)
# packages/utils/CLAUDE.md — Package tier (inherits library tier above)

## Scope
String utilities, date formatting, data transformation helpers.
This package is zero-dependency and must stay that way.

## Constraints
- No async functions — synchronous transformations only
- Every export must be tree-shakeable
- Function names follow the verb-noun pattern: formatDate, parseAmount, slugify

Verdict: The best pattern for large orgs. Requires discipline to maintain tier separation but pays off at scale.


Pattern 4: Workspace-Aware Tool Configs

Structure:

monorepo/
├── CLAUDE.md
├── .claude/
│   ├── agents/
│   │   ├── nx-task-runner.md    # Nx-specific agent
│   │   ├── build-verifier.md
│   │   └── cross-package-auditor.md
│   └── settings.json
└── packages/
    └── [packages with AGENTS.md]

When it works: Teams already using workspace-level tooling (Nx, Turborepo) who want to wrap those tools in purpose-built AI agents.

When it breaks: When the agents are too tightly coupled to the build tool version. An Nx 18 agent configuration may not work correctly after upgrading to Nx 19 if the command API changed.

This pattern focuses less on CLAUDE.md hierarchy and more on subagents that understand the workspace orchestration layer. The value is in the agents, not in the per-package files.

---
name: nx-task-runner
description: Use when you need to run Nx tasks, understand the dependency graph, check affected packages, or debug Nx cache behavior. Knows Nx commands and the project graph.
tools: Bash, Read, Glob
model: sonnet
maxTurns: 10
---

You understand the Nx build system for this monorepo.

## Key commands
- `nx show project <name>` — show project configuration and targets
- `nx graph` — open dependency graph in browser (CI: use --file=output.json)
- `nx affected --target=build --base=main` — only build what changed
- `nx run-many --target=test --all` — test everything
- `nx reset` — clear Nx cache if builds are producing stale output
- `nx show projects --affected --base=main --head=HEAD` — list affected projects

## Cache behavior
Nx caches task outputs by input hash. If you change a file and the build isn't reflecting it:
1. Check `nx show project <name>` for the cacheable operations
2. If the target has `cache: true`, old output may be served
3. `nx reset` clears all local cache
4. Remote cache (Nx Cloud) may still serve cached results — check Nx Cloud dashboard

## Affected calculation
Nx's affected detection is based on `nx.json` implicitDependencies and the project graph.
- A change to a library marks all downstream dependents as affected
- Changes to `nx.json` or `project.json` may mark everything as affected

## What you should NOT do
- Never run `nx deploy` or `nx affected --target=deploy` — deployments go through CI
- Never modify nx.json without explicit user instruction

Verdict: Powerful for teams with mature workspace tooling. Overkill for smaller setups or teams unfamiliar with Nx’s execution model.


Pattern 5: Domain-Driven Structure

Structure:

monorepo/
├── CLAUDE.md                     # Organization-wide
├── domains/
│   ├── billing/
│   │   ├── CLAUDE.md             # Billing domain rules
│   │   ├── billing-api/
│   │   ├── billing-ui/
│   │   └── billing-worker/
│   ├── identity/
│   │   ├── CLAUDE.md             # Identity domain rules
│   │   ├── auth-service/
│   │   └── user-management/
│   └── analytics/
│       ├── CLAUDE.md             # Analytics domain rules
│       └── [packages]

When it works: Product-aligned teams, domain-driven design already in the codebase, AI assistants that need business context as much as technical context.

When it breaks: Technical concerns (infrastructure, shared utilities) don’t map cleanly to domains.

Domain-level CLAUDE.md files carry something that per-package files often miss: the why. They can explain business rules, data ownership boundaries, and integration contracts between domains in a way that purely technical package configs can’t.

# domains/billing/CLAUDE.md

## Domain Overview
The billing domain handles subscription lifecycle, invoice generation, and payment processing.
This domain owns the canonical source of truth for subscription status — other domains must
query billing's API rather than directly accessing billing tables.

## Business Rules
- Never process a payment without an idempotency key — duplicate charges are a P0 incident
- All pricing changes must go through the price_override approval workflow (see billing-api/docs/pricing.md)
- Subscription cancellations are soft-deleted and retained for 90 days (legal requirement)
- Tax calculation uses the rates service — never hardcode tax rates

## Package Responsibilities
- billing-api: HTTP API and webhook handlers (Stripe events)
- billing-ui: Customer-facing subscription management UI
- billing-worker: Async invoice generation and dunning logic

## Integration Points
- Reads from: identity domain (user status)
- Writes to: None outside domain
- Events emitted: subscription.created, subscription.cancelled, payment.succeeded, payment.failed

## Data Sensitivity
All billing data is PCI-DSS scoped. Do not log card numbers, CVVs, or full PANs.
Stripe handles cardholder data — we only store tokens.

Verdict: Worth the structural investment if your team is organized by domain. The business context in domain-level files produces noticeably better AI output for domain-logic tasks.


Pattern 6: Generated Config (Template-Based)

Structure:

monorepo/
├── scripts/
│   ├── templates/
│   │   ├── library-package.claude.md.tpl
│   │   ├── app-package.claude.md.tpl
│   │   └── service-package.claude.md.tpl
│   └── generate-configs.ts
├── packages/
│   └── [packages with generated CLAUDE.md files]

When it works: 50+ packages, strong engineering platform team, packages that follow consistent templates, large onboarding volume.

When it breaks: When the generator doesn’t handle edge cases, or when teams start adding “just this once” manual overrides that prevent future regeneration.

The template approach solves the maintenance drift problem mechanically: configs are regenerated from templates, divergences are flagged by CI, and updates to templates propagate everywhere automatically.

// scripts/generate-configs.ts
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { glob } from "glob";

const TEMPLATE_MAP: Record<string, string> = {
  library: "templates/library-package.claude.md.tpl",
  app: "templates/app-package.claude.md.tpl",
  service: "templates/service-package.claude.md.tpl",
};

interface PackageJson {
  name: string;
  "ai-config"?: { type: "library" | "app" | "service"; overrides?: string };
}

async function generateConfigs() {
  const packageJsonPaths = await glob("packages/*/package.json");

  for (const pkgJsonPath of packageJsonPaths) {
    const pkg: PackageJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
    const packageType = pkg["ai-config"]?.type ?? "library";
    const templatePath = TEMPLATE_MAP[packageType];
    const packageDir = pkgJsonPath.replace("/package.json", "");

    let template = readFileSync(templatePath, "utf-8");
    template = template.replace("{{PACKAGE_NAME}}", pkg.name);
    template = template.replace("{{PACKAGE_DIR}}", packageDir);

    // Preserve manual overrides section
    const overridesMarker = "## Manual Overrides (preserved during regeneration)";
    const existingConfig = existsSync(join(packageDir, "CLAUDE.md"))
      ? readFileSync(join(packageDir, "CLAUDE.md"), "utf-8")
      : "";
    const existingOverrides = existingConfig.includes(overridesMarker)
      ? existingConfig.split(overridesMarker)[1]
      : "";

    const output = template + (existingOverrides ? `\n${overridesMarker}\n${existingOverrides}` : "");
    writeFileSync(join(packageDir, "CLAUDE.md"), output);
    console.log(`Generated: ${packageDir}/CLAUDE.md`);
  }
}

generateConfigs();

CI check to catch drift:

# .github/workflows/ai-config-check.yml
- name: Check AI config consistency
  run: |
    pnpm ts-node scripts/generate-configs.ts
    if [ -n "$(git diff --name-only | grep CLAUDE.md)" ]; then
      echo "CLAUDE.md files are out of sync with templates. Run scripts/generate-configs.ts and commit."
      git diff -- "*.md" | grep "CLAUDE.md" | head -20
      exit 1
    fi

Verdict: Essential for 50+ packages. The upfront investment in templates and the generator script pays for itself within a few months of avoiding manual drift.


Pattern 7: Hybrid (Generated Base + Manual Overrides)

Structure:

monorepo/
├── packages/
│   └── special-package/
│       └── CLAUDE.md
│           # Generated section (auto-updated)
│           # --- Manual Overrides (preserved) ---
│           # Package-specific rules that don't fit the template

This is Pattern 6 with an explicit escape hatch. The generator handles the base configuration, and a clearly marked section at the bottom of each file is preserved across regenerations.

The discipline required: the manual overrides section should be genuinely exceptional. If you find yourself writing overrides for most packages, the template is wrong.

# CLAUDE.md — packages/legacy-payments
# Generated from: scripts/templates/service-package.claude.md.tpl
# Last regenerated: 2026-04-15
# Generator version: 2.4.1
# DO NOT edit the generated section. Run `pnpm generate:configs` to update.

## [Generated Content Below]
[...standard service package content...]

## Manual Overrides (preserved during regeneration)
This package uses a legacy PHP backend for payment processing (being migrated — target Q3 2026).
Do not suggest Node.js database patterns here. The PHP service is at services/legacy-php/.

When modifying the payment flow, read services/legacy-php/ARCHITECTURE.md first.
The integration surface is a REST API — never directly access the legacy database tables.

Verdict: The most robust pattern for large monorepos. Handles both the consistency problem (templates) and the exceptions-are-real-life problem (overrides section).


Pattern Comparison Table

PatternBest forPackage countMaintenance costToken per session
Single RootHomogeneous small repos< 15Very lowFixed, high
Per-Package FlatIndependent teams15–40MediumLow, bounded
HierarchicalCategory-organized repos40–100MediumLow, 3-tier
Workspace-AwareHeavy build tool usersAnyHigh (agents)Low
Domain-DrivenDDD codebases20–80MediumLow, domain-bounded
GeneratedPlatform-led orgs50+Low (automated)Low
Hybrid (Generated + Overrides)Complex large repos50+Very lowLow

Tool-Specific Walkthroughs

The patterns above are tool-agnostic. What follows is how each pattern maps to specific workspace tools.

Nx Workspaces

Nx differs from the others in one important way: it has a rich project graph that the AI tools can’t automatically read. Your CLAUDE.md and AGENTS.md files need to make the project graph human-readable, because Claude can’t run nx graph and reason about the output without help.

Recommended: Hierarchical (Pattern 3) + Workspace-Aware agents (Pattern 4)

# CLAUDE.md — Root (Nx workspace)

## Nx Essentials
- Workspace manager: pnpm
- Build orchestrator: Nx 19 (see nx.json for target defaults)
- Run affected only: `nx affected --target=<target> --base=main`
- Run single project: `nx run <project>:<target>`
- Reset Nx cache: `nx reset`

## Project Organization
- Libraries: libs/ (internal, not published)
- Applications: apps/ (deployable)
- Shared utilities: libs/shared/

## Dependency Rules (enforced by ESLint @nx/enforce-module-boundaries)
- apps/ can import from libs/
- libs/feature-* can import from libs/data-access-* and libs/ui-*
- libs/data-access-* cannot import from libs/feature-*
- libs/ui-* cannot import from libs/data-access-*
- Never import across application boundaries

## Nx Cache
Targets configured as cacheable in nx.json will serve cached results unless inputs change.
If a change isn't reflected in build output, check that the changed file is in the target's input set.

Nx-specific subagent for affected analysis:

---
name: nx-affected-analyzer
description: Use before any significant change to understand which projects will be affected. Run this before touching shared libraries or changing nx.json.
tools: Bash, Read
model: sonnet
maxTurns: 8
---

Your job is to help the user understand the downstream impact of a proposed change in this Nx workspace.

Steps:
1. Ask (or determine from context) which file or library is being modified
2. Run `nx show projects --affected --base=main` to get the current affected set
3. If the change isn't committed yet, simulate it: `nx show project <affected-lib> --json` to see dependents
4. Run `nx graph --file=graph.json` and read the relevant sections for the changed project
5. Report: which projects are affected, which targets will run in CI, estimated CI time impact

For changes to root files (package.json, nx.json, tsconfig.base.json):
- These may affect every project. Warn explicitly.
- Check if the change is to an implicit dependency defined in nx.json

Always end with a recommendation: is this safe to proceed with, or should the user checkpoint with CI first?

Turborepo Workspaces

Turborepo’s key differentiator is pipeline configuration in turbo.json — the dependency graph between tasks, not just packages. Your AI config should reflect this.

Recommended: Hierarchical (Pattern 3)

# CLAUDE.md — Root (Turborepo workspace)

## Turborepo Pipeline
Task dependencies are defined in turbo.json. Key pipeline:
- build depends on upstream build (^build)
- test depends on local build (build)
- lint has no dependencies (runs in parallel)

## Running Tasks
- All packages: `pnpm turbo <task>`
- Single package: `pnpm turbo <task> --filter=<package>`
- Filter with deps: `pnpm turbo <task> --filter=<package>...`
- Dry run (see what would run): `pnpm turbo build --dry-run`

## Cache Behavior
Turborepo caches by content hash. Cache misses happen when:
- Source files in the package change
- Dependencies listed in turbo.json inputs change
- Package.json or turbo.json changes

To force a cache miss: `pnpm turbo build --force`
To delete local cache: `rm -rf .turbo`

## Adding a New Package
1. Create packages/<name>/package.json with name @company/<name>
2. Run pnpm install to update lockfile
3. Add CLAUDE.md to packages/<name>/
4. If it's a library, update packages/<name>/turbo.json if task dependencies differ from defaults
5. Run `pnpm turbo build --filter=@company/<name>` to verify it builds

## Turborepo Remote Cache
Remote cache is configured via TURBO_TOKEN and TURBO_TEAM env vars in CI.
Do not commit these values. CI reads them from GitHub Actions secrets.

Per-package for Turborepo:

# CLAUDE.md — packages/design-tokens

## Package Identity
Name: @company/design-tokens
Purpose: Single source of truth for design system tokens (colors, spacing, typography)
Consumers: apps/web, apps/mobile, libs/ui-*

## Build Process
Input: src/tokens.json (design tool export)
Output: dist/ (CSS variables, JS constants, TypeScript types)
Build command: `pnpm build` (runs style-dictionary)

## Turborepo Pipeline Impact
Changes to this package invalidate the build cache for ALL consumers.
Before making changes, run `pnpm turbo build --filter=@company/design-tokens... --dry-run`
to see how many downstream packages will need to rebuild.

## Adding New Tokens
1. Edit src/tokens.json
2. Run `pnpm build` to generate outputs
3. The generated files in dist/ are committed (required for downstream TypeScript resolution)
4. Never manually edit dist/ — it's generated

pnpm Workspaces (Without a Build Orchestrator)

Some repos use pnpm workspaces without Nx or Turborepo for orchestration. This is common in repos under 20 packages or repos that prioritize simplicity. The AI tool setup is simpler, but the lack of a dependency graph means the AI has less structured context to work with.

Recommended: Per-Package Flat (Pattern 2) or Hierarchical (Pattern 3)

# CLAUDE.md — Root (pnpm workspaces, no orchestrator)

## Workspace Setup
- Package manager: pnpm 9
- Workspaces defined in pnpm-workspace.yaml
- No build orchestrator — scripts run independently per package

## Finding Packages
- `pnpm list -r` — list all workspace packages
- `pnpm recursive run build` — build all (no dependency order guarantee)
- `pnpm --filter <name> <script>` — run script in specific package

## Dependency Management
- Internal packages: referenced as `"@company/pkg": "workspace:*"`
- After adding a new internal dependency: `pnpm install` to update symlinks
- lockfile: pnpm-lock.yaml — always commit lockfile changes

## No Dependency Graph
Unlike Nx/Turborepo, there is no task dependency graph.
Building packages in the wrong order causes errors.
Known build order: shared-types → utils → [everything else]
If you change shared-types, rebuild utils and all consumers manually.

The “no dependency graph” acknowledgment is key. Claude can’t infer the build order from the workspace configuration alone. Making it explicit prevents a common class of errors where the AI suggests running a build that will fail because a dependency hasn’t been rebuilt.

Bazel

Bazel is used in larger organizations where hermetic builds, remote execution, and multi-language support matter. It’s meaningfully more complex than the other tools, and the AI setup needs to reflect that complexity without becoming overwhelming.

Recommended: Domain-Driven (Pattern 5) + Workspace-Aware agents (Pattern 4)

Bazel’s BUILD files are the source of truth for the dependency graph — not a CLAUDE.md hierarchy. Your AI config should acknowledge this explicitly.

# CLAUDE.md — Root (Bazel workspace)

## Build System
Bazel with Bzlmod (MODULE.bazel). No npm/pnpm/yarn.

## Key Commands
- Build target: `bazel build //path/to/package:target`
- Build all: `bazel build //...`
- Test target: `bazel test //path/to/package:test-target`
- Test all: `bazel test //...`
- Query dependents: `bazel query "rdeps(//..., //path/to:lib)" --output=label`
- Query dependencies: `bazel query "deps(//path/to:target)" --output=label`

## Understanding the Dependency Graph
Bazel's BUILD files define the dependency graph, not CLAUDE.md.
Before modifying a library:
1. Run `bazel query "rdeps(//..., //<path>:<target>)"` to find all dependents
2. Changes to a library require rebuilding all rdeps
3. Breaking changes to a public API require updating all consumers in the same commit

## Rules in Use
- TypeScript: @aspect-build/rules_ts
- Python: rules_python
- Docker: rules_docker
- Proto: rules_proto_grpc

## What You Should NOT Do
- Never run `npm install` — Bazel manages dependencies via MODULE.bazel
- Never modify generated files in bazel-out/
- Never hardcode paths that contain bazel-out/ — use $(BINDIR) in build rules

Bazel-specific BUILD file agent:

---
name: bazel-build-analyzer
description: Use when adding new Bazel targets, troubleshooting build failures, understanding dependency chains, or checking what's affected by a change to a BUILD file.
tools: Bash, Read, Grep, Glob
model: sonnet
maxTurns: 15
---

You are a Bazel build system expert for this workspace.

## Key capabilities

**Dependency analysis**
```bash
# Find everything that depends on a target
bazel query "rdeps(//..., //libs/auth:auth_lib)" --output=label

# Find everything a target depends on
bazel query "deps(//apps/api:server)" --output=label

# Find the path between two targets
bazel query "somepath(//apps/api:server, //libs/legacy:old_lib)"

BUILD file reading When asked about a package, always read its BUILD file first. cat path/to/package/BUILD or cat path/to/package/BUILD.bazel

Failure diagnosis For build failures:

  1. Read the full error output — Bazel errors are verbose but precise
  2. Check the failing target’s BUILD file
  3. Verify that all deps in the BUILD file actually exist as targets
  4. For “undeclared inclusion” errors: a header file is used but not in hdrs or deps

What you will NOT do

  • Suggest modifying bazel-out/ files
  • Run bazel run :target without confirming with the user what the target does
  • Suggest bazel clean unless specifically asked — it clears the entire cache and is slow

---

## Token Budget Measurement

You can't optimize what you can't measure. Here are the techniques for measuring actual token consumption from your AI config files.

### Measuring Context Load at Session Start

```bash
#!/bin/bash
# measure-context-load.sh
# Run from the package directory you're working in to simulate
# what an AI tool loads at session start

PACKAGE_DIR="${1:-.}"
TOTAL_WORDS=0

# Walk from cwd to repo root, collecting CLAUDE.md files
dir="$PACKAGE_DIR"
while [ "$dir" != "/" ]; do
  if [ -f "$dir/CLAUDE.md" ]; then
    words=$(wc -w < "$dir/CLAUDE.md")
    echo "$dir/CLAUDE.md: $words words"
    TOTAL_WORDS=$((TOTAL_WORDS + words))
  fi
  dir=$(dirname "$dir")
done

# Also check AGENTS.md
dir="$PACKAGE_DIR"
while [ "$dir" != "/" ]; do
  if [ -f "$dir/AGENTS.md" ]; then
    words=$(wc -w < "$dir/AGENTS.md")
    echo "$dir/AGENTS.md: $words words"
    TOTAL_WORDS=$((TOTAL_WORDS + words))
  fi
  dir=$(dirname "$dir")
done

echo "---"
echo "Total words: $TOTAL_WORDS"
echo "Estimated tokens (words × 1.3): $((TOTAL_WORDS * 13 / 10))"
echo ""
if [ $TOTAL_WORDS -gt 2000 ]; then
  echo "WARNING: Context load is high. Consider trimming or using @ references."
fi

Run it across all packages to find outliers:

# Find the packages with the heaviest CLAUDE.md load
for pkg in packages/*/; do
  total=$(bash measure-context-load.sh "$pkg" 2>/dev/null | grep "Total words" | awk '{print $3}')
  echo "$total $pkg"
done | sort -rn | head -20

Per-Package Token Budget Targets

These are practical targets based on what keeps AI output quality high without burning context:

Context levelTarget word countTarget tokens
Global (~/.claude/CLAUDE.md)< 200 words< 260 tokens
Repo root CLAUDE.md< 400 words< 520 tokens
Category/domain CLAUDE.md< 300 words< 390 tokens
Package CLAUDE.md< 300 words< 390 tokens
Total per session< 1,000 words< 1,300 tokens

If you’re consistently over 1,000 words across all loaded files, the AI’s attention is split between your conventions and the actual code. The conventions start to win, and code quality suffers in subtle ways.

Detecting Redundancy Between Levels

# Find rules that appear in both root and package configs (possible duplication)
# Simple word-level check — not perfect but catches obvious overlap
grep -h "^-" CLAUDE.md packages/*/CLAUDE.md 2>/dev/null | \
  sort | uniq -d | head -20

More sophisticated: run a diff between your most common phrases across files and identify copy-paste duplication that could be consolidated.

Using Claude Code’s Own Cost Tracking

Claude Code exposes session-level cost data through the /cost command (available in interactive mode). For automated tracking:

# In your CI pipeline, capture token usage per task type
# Claude Code logs session data to ~/.claude/logs/ by default
ls -lt ~/.claude/logs/ | head -5
cat ~/.claude/logs/$(ls -t ~/.claude/logs/ | head -1) | \
  python3 -c "
import sys, json
for line in sys.stdin:
    try:
        d = json.loads(line)
        if 'usage' in d:
            print(f\"Input: {d['usage'].get('input_tokens', 0)}, Output: {d['usage'].get('output_tokens', 0)}\")
    except:
        pass
"

CI/CD Integration Patterns

CI is where AI tool configurations get stress-tested. A config that works fine locally often fails in CI because the working directory, environment variables, and available context are different.

Pattern A: Pre-flight Config Validation

Before any AI-assisted CI task runs, validate that the config files are loadable and within budget:

# .github/workflows/ai-config-validate.yml
name: AI Config Validation
on: [push, pull_request]

jobs:
  validate-configs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check CLAUDE.md word counts
        run: |
          max_words=400
          failed=0
          for f in $(find . -name "CLAUDE.md" -not -path "*/node_modules/*"); do
            words=$(wc -w < "$f")
            if [ "$words" -gt "$max_words" ]; then
              echo "FAIL: $f is $words words (max: $max_words)"
              failed=1
            else
              echo "OK: $f ($words words)"
            fi
          done
          exit $failed

      - name: Check AGENTS.md word counts
        run: |
          max_words=400
          failed=0
          for f in $(find . -name "AGENTS.md" -not -path "*/node_modules/*"); do
            words=$(wc -w < "$f")
            if [ "$words" -gt "$max_words" ]; then
              echo "FAIL: $f is $words words (max: $max_words)"
              failed=1
            else
              echo "OK: $f ($words words)"
            fi
          done
          exit $failed

Pattern B: Package-Scoped AI Review in PR Checks

When a PR touches packages/api/, run a package-scoped AI review that only loads context relevant to that package:

# .github/workflows/ai-pr-review.yml
name: AI PR Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  determine-affected-packages:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.detect.outputs.packages }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: detect
        run: |
          # Get list of changed packages
          changed=$(git diff --name-only origin/main...HEAD | grep "^packages/" | cut -d/ -f2 | sort -u | jq -R -s -c 'split("\n")[:-1]')
          echo "packages=$changed" >> $GITHUB_OUTPUT

  ai-review:
    needs: determine-affected-packages
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: ${{ fromJSON(needs.determine-affected-packages.outputs.packages) }}
    steps:
      - uses: actions/checkout@v4
      - name: Run scoped AI review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          # cd into the package directory so Claude loads only relevant CLAUDE.md files
          cd packages/${{ matrix.package }}
          claude --no-interactive --max-turns 5 "Review the git diff for packages/${{ matrix.package }} against main. Check for: violations of conventions in CLAUDE.md, missing tests, obvious bugs. Output a structured report with: PASS/FAIL, issues found, severity."

Pattern C: Drift Detection for Generated Configs

If you’re using Pattern 6 or 7 (generated configs), add a CI check that fails the build when configs drift from templates:

- name: Validate AI config sync
  run: |
    # Regenerate all configs
    node scripts/generate-configs.js

    # Fail if any CLAUDE.md or AGENTS.md changed
    if git diff --name-only | grep -E "(CLAUDE|AGENTS)\.md"; then
      echo "Error: AI config files are out of sync with templates."
      echo "Run: node scripts/generate-configs.js && git add -A && git commit -m 'chore: sync AI configs'"
      echo ""
      echo "Changed files:"
      git diff --name-only | grep -E "(CLAUDE|AGENTS)\.md"
      exit 1
    fi

    echo "All AI config files are in sync with templates."

Pattern D: Subagent Smoke Tests

For projects that use purpose-built subagents, test that agents load correctly and produce coherent output:

#!/bin/bash
# scripts/test-agents.sh
# Smoke test for project subagents

AGENTS_DIR=".claude/agents"
FAILED=0

for agent_file in $AGENTS_DIR/*.md; do
  agent_name=$(grep "^name:" "$agent_file" | cut -d' ' -f2)
  echo "Testing agent: $agent_name"

  # Check required fields
  for field in name description tools model; do
    if ! grep -q "^$field:" "$agent_file"; then
      echo "  FAIL: missing required field '$field'"
      FAILED=1
    fi
  done

  # Check model is valid
  model=$(grep "^model:" "$agent_file" | cut -d' ' -f2)
  if [[ ! "$model" =~ ^(sonnet|haiku|opus)$ ]]; then
    echo "  WARN: model '$model' is not a recognized short name"
  fi

  echo "  OK: $agent_name"
done

exit $FAILED

Pitfalls and FAQ

Q: We have 60 packages and our root CLAUDE.md is approaching 800 words. What do we cut first?

Cut examples before rules. Examples in config files feel helpful (“here’s what a good commit message looks like”) but they consume a lot of tokens for information that’s usually obvious from the rule itself. Move examples to a referenced doc (@docs/examples.md) and keep only the rules in the CLAUDE.md hierarchy.

After examples, look for rules that are covered by your tooling: if your linter enforces a rule, you don’t need to tell Claude about it in CLAUDE.md — the linter will catch it and the error output gives Claude enough context.

Q: Should AGENTS.md and CLAUDE.md coexist in the same directory?

Yes, and they serve different purposes. CLAUDE.md is read by Claude Code when you’re in an interactive session. AGENTS.md is read by OpenAI’s Codex, Google’s Gemini CLI, and other tools that look for AGENTS.md. If your team uses multiple AI tools, maintain both. They can share ~80% of their content; the tool-specific sections differ.

For a monorepo with multiple tools in use:

# Check which AI config files exist at which levels
find . -name "CLAUDE.md" -o -name "AGENTS.md" | sort | \
  awk -F'/' '{depth=NF-1; printf "%*s%s\n", depth*2, "", $0}'

Q: Our subagents keep picking the wrong package CLAUDE.md. How do we fix agent routing?

This is almost always a problem with the subagent description field. The description is Claude’s primary signal for which agent to invoke. It should answer “when should Claude choose this agent?” not “what does this agent do?”.

Bad description: "Handles database operations and Prisma migrations."

Better: "Use when working on database schema changes, writing Prisma migrations, or debugging database query performance. Do not use for general API route development."

The “do not use for” clause is surprisingly effective at preventing misroutes.

Q: We’re migrating from Turborepo to Nx. How do we handle the transition in AI configs?

Create a migration guide in your root CLAUDE.md that explicitly documents the transition state:

## Build System (In Transition: Turborepo → Nx)
Migration in progress. Target completion: Q3 2026.

Current state:
- packages/ — still using Turborepo (pnpm turbo <task>)
- apps/ — migrated to Nx (nx run <project>:<target>)

If you're unsure which system a package uses, check for nx.json in the package root.
Packages with nx.json use Nx. Others use Turborepo.

DO NOT use turbo commands for apps/ packages or nx commands for packages/ packages.

Explicit “in transition” documentation prevents a class of AI mistakes that otherwise take hours to debug.

Q: How do we handle AI configs for packages that are being deprecated?

Add a deprecation notice to the top of the package’s CLAUDE.md:

# CLAUDE.md — packages/legacy-auth
# DEPRECATED: This package is being replaced by packages/auth-v2
# Do not add new features here.
# Do not fix non-critical bugs here — fix them in auth-v2 instead.
# Target removal: 2026-Q3

[...remaining config for maintenance purposes...]

The explicit deprecation instruction is important: without it, Claude will happily add features to a deprecated package because the code is valid and the tests pass.

Q: We’re hitting rate limits when running AI-assisted CI across 50+ affected packages. How do we throttle?

Use GitHub Actions matrix concurrency limits:

jobs:
  ai-review:
    strategy:
      matrix:
        package: ${{ fromJSON(needs.packages.outputs.list) }}
      max-parallel: 5    # Never run more than 5 package reviews simultaneously
    concurrency:
      group: ai-review-${{ matrix.package }}
      cancel-in-progress: true

Pair this with a package-level cache: if the package’s source files haven’t changed since the last review, skip the review job and reuse the previous result.

Q: Should we commit .claude/settings.json?

Yes, with restrictions. Commit the project-level settings that enforce team-wide standards — denied commands, model selection, environment variables. Do not commit personal settings like API keys, personal MCP servers, or individual preferences. Use .claude/settings.local.json for those and add it to .gitignore.

For a 50+ package monorepo, the project settings file often includes:

{
  "model": "sonnet",
  "permissions": {
    "deny": [
      "Bash(npm install:*)",
      "Bash(yarn install:*)",
      "Bash(git push --force:*)",
      "Bash(bazel run :deploy:*)",
      "Bash(terraform apply:*)"
    ]
  },
  "env": {
    "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "8000",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
  }
}

The deny list is particularly valuable in a large monorepo where a confused AI could accidentally run a command that has broad impact (force push to main, deploy to production).


Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules