Monorepo Navigation skill for Claude Code

Run the affected tasks, not the whole repo. Claude identifies the workspace tooling first, then runs only the affected tasks through Turborepo, Nx, or pnpm filters instead of testing everything from the root. It adds dependencies to the package that imports them, uses the workspace protocol for internal packages, respects module boundaries, and debugs cache misses by inspecting task hashes, inputs and declared outputs.

Name
monorepo-navigation
Size
156 lines · ≈2,000 tokens
Files
1
License
MIT
Updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Works effectively inside JavaScript and TypeScript monorepos built on pnpm, npm, or Yarn workspaces with Turborepo or Nx, by reading the package graph, running only affected tasks with filters, adding dependencies to the right package, respecting package boundaries, and keeping task caching correct. Use when the user works in a workspace or monorepo, asks which packages a change affects, how to run build or test for one package, where to add a dependency, why the cache misses, or how to share code between apps and packages.

Try asking

  • Which packages does my change affect, and run only their tests
  • Add zod to the api package in our pnpm monorepo
  • Turbo keeps serving a stale build from cache — figure out why

Use it when

  • The repo has pnpm, npm, or Yarn workspaces, Turborepo, or Nx
  • You need to build or test one package and its dependents
  • A dependency must be added or aligned across packages
  • Task caching or CI times look wrong

Skip it when

How to check the result

The dry-run or affected listing shows exactly the expected packages, dependency changes touch only the owning package and root lockfile, and an isolated build of the app succeeds.

The common mistake it prevents

Relying on a hoisted phantom dependency that works locally but fails in Docker or CI because the importing package never declared it.

~/.claude/skills/monorepo-navigation/SKILL.md

--- name: monorepo-navigation description: Works effectively inside JavaScript and TypeScript monorepos built on pnpm, npm, or Yarn workspaces with Turborepo or Nx, by reading the… license: MIT ---

Working well in a monorepo means you always run the smallest correct set of tasks, change dependencies in the package that owns them, never import across a boundary the repo forbids, and keep the task cache trustworthy. Running npm test at the root on every edit is how monorepos become slow.

Ground rules

  • Identify the tooling before running anything: package manager, task runner, and how packages are discovered. Use the exact versions pinned in the repo.
  • Run tasks through the task runner (turbo/nx) when one exists, so dependency ordering and caching apply. Run the package manager directly only for installs.
  • Install from the repo root only; one lockfile for the whole workspace.
  • Internal dependencies use the workspace protocol ("@acme/ui": "workspace:*"), never a published version or relative file: path, unless the repo already does otherwise.
  • Import other packages only through their public entry (exports/main), never via deep relative paths like ../../packages/ui/src/internal.

Orient

cat package.json | sed -n '/"packageManager"/p;/"workspaces"/,/]/p'
cat pnpm-workspace.yaml 2>/dev/null
ls turbo.json nx.json lerna.json 2>/dev/null
pnpm ls -r --depth -1                 # list workspace packages (pnpm)
npm query .workspace | grep '"name"'  # list workspace packages (npm)
npx nx show projects                  # Nx
npx turbo ls                          # Turborepo 2.x

Find which package owns a file: walk up from the file to the nearest package.json and read its name.

Tooling map

NeedpnpmnpmYarn (berry)
Run script in one packagepnpm --filter @acme/web devnpm run dev -w @acme/webyarn workspace @acme/web dev
Run in all packagespnpm -r run buildnpm run build --workspaces --if-presentyarn workspaces foreach -A run build
Topological orderdefault with -rnot guaranteedyarn workspaces foreach -At run build
Add dep to one packagepnpm add zod --filter @acme/apinpm install zod -w @acme/apiyarn workspace @acme/api add zod
Add internal deppnpm add @acme/ui --workspace --filter @acme/webadd "@acme/ui": "*" then npm installyarn workspace @acme/web add @acme/ui@workspace:^
Root dev toolpnpm add -Dw typescriptnpm install -D typescript at rootyarn add -D typescript at root
Why is X herepnpm why -r Xnpm explain Xyarn why X

pnpm filter selectors (combine as needed):

pnpm --filter @acme/web build            # one package
pnpm --filter "@acme/web..." build       # web and everything it depends on
pnpm --filter "...@acme/ui" test         # ui and everything that depends on it
pnpm --filter "...[origin/main]" test    # changed since origin/main, plus dependents
pnpm --filter "./packages/**" lint       # by path

Turborepo

turbo run build --filter=@acme/web               # web plus its dependencies' builds (via ^build)
turbo run test --filter="...[origin/main]"       # changed packages and their dependents
turbo run lint test --affected                   # Turborepo 2.x: affected vs. base branch
turbo run build --dry=json                       # what would run, with hashes; no execution
turbo run build --graph=graph.html               # visualize the task graph
turbo run build --summarize                      # writes a run summary for cache debugging
turbo prune @acme/web --docker                   # minimal subset for a Docker build

turbo.json essentials (v2 uses tasks; v1’s pipeline is renamed):

{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] },
    "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] },
    "lint": {},
    "dev": { "cache": false, "persistent": true }
  }
}

^build means “build my dependencies first”. Env vars that change output must be listed in env (or globalEnv); Turborepo’s strict env mode filters undeclared vars out of task environments.

Nx

npx nx run web:build                    # one target for one project
npx nx run-many -t build test -p web api
npx nx affected -t lint test build      # affected vs. defaultBase (usually main)
npx nx affected -t test --base=origin/main --head=HEAD
npx nx show project web                 # resolved targets, inputs, outputs
npx nx graph                            # interactive project graph
npx nx reset                            # clear cache and daemon when results look stale

Boundaries: Nx enforces tags with the @nx/enforce-module-boundaries ESLint rule (e.g. type:ui may not depend on type:data-access of another scope). Read depConstraints before adding an import across scopes.

Decisions

QuestionAnswer
Where does a new dependency go?In the package that imports it. Root only for repo-wide tooling (lint, format, turbo, typescript)
Shared code for two apps?A new internal package under packages/, with an explicit exports map, not a shared folder imported by relative path
Internal package: compiled or source?Default to source-only (“just-in-time”) packages consumed by the app’s bundler for internal-only libs; compile (with build task and dist outputs) if consumed by Node without a bundler, published, or slow to typecheck
Version mismatch of the same dep across packagesAlign versions (pnpm catalog: in pnpm-workspace.yaml, or syncpack / npx @manypkg/cli check) unless there is a documented reason
Which tests to run before pushingAffected set vs. the base branch (--affected / ...[origin/main]), not everything, not just one package
CI is slowRemote cache, affected-only runs, and correct outputs/inputs before adding more machines

TypeScript in workspaces

  • Each package has its own tsconfig.json extending a shared base (packages/tsconfig/base.json or root tsconfig.base.json).
  • With compiled packages, use project references ("references": [{ "path": "../ui" }], "composite": true) and tsc -b for incremental builds.
  • tsconfig paths aliases that point into other packages’ src bypass package boundaries and break in production builds; prefer workspace dependencies with exports.
  • "exports" in package.json controls what consumers can import; include a types condition first when shipping types.

Gotchas

  • Phantom dependencies: code imports a package that is installed for a sibling and hoisted. It works locally and fails in isolated builds or Docker. pnpm’s strict layout surfaces these; fix by adding the dependency to the importing package.
  • Cache poisoning: missing outputs means cache hits restore nothing; missing inputs/env means stale artifacts are served after a change. When a result looks stale, check the task hash inputs (--dry=json, nx show project).
  • Running the package manager inside a package directory can create a nested lockfile or install in the wrong scope. Run from the root with filters.
  • Circular dependencies between packages break topological ordering; Nx and pnpm warn about them. Extract the shared piece into a third package.
  • --filter=...[origin/main] on a shallow clone fails or computes everything; CI needs fetch-depth: 0 (or enough history) to find the merge base.
  • Changing a root config (base tsconfig, ESLint config, lockfile) affects all packages; tools usually treat it as a global input, so expect full rebuilds.

Verification

  • Before pushing: run lint, typecheck, and tests for the affected set and confirm the list of packages matches what you expect (--dry=json, nx show projects --affected).
  • After adding a dependency: git diff shows changes only in the owning package’s package.json and the root lockfile.
  • A clean, isolated build of the changed app (turbo prune + install, or CI) succeeds, proving there are no phantom dependencies.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc