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
- You are new to the repo and need an overall architecture map — use codebase-onboarding
- You are upgrading framework versions across packages — use dependency-upgrades
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.
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 relativefile: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
| Need | pnpm | npm | Yarn (berry) |
|---|---|---|---|
| Run script in one package | pnpm --filter @acme/web dev | npm run dev -w @acme/web | yarn workspace @acme/web dev |
| Run in all packages | pnpm -r run build | npm run build --workspaces --if-present | yarn workspaces foreach -A run build |
| Topological order | default with -r | not guaranteed | yarn workspaces foreach -At run build |
| Add dep to one package | pnpm add zod --filter @acme/api | npm install zod -w @acme/api | yarn workspace @acme/api add zod |
| Add internal dep | pnpm add @acme/ui --workspace --filter @acme/web | add "@acme/ui": "*" then npm install | yarn workspace @acme/web add @acme/ui@workspace:^ |
| Root dev tool | pnpm add -Dw typescript | npm install -D typescript at root | yarn add -D typescript at root |
| Why is X here | pnpm why -r X | npm explain X | yarn 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
| Question | Answer |
|---|---|
| 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 packages | Align versions (pnpm catalog: in pnpm-workspace.yaml, or syncpack / npx @manypkg/cli check) unless there is a documented reason |
| Which tests to run before pushing | Affected set vs. the base branch (--affected / ...[origin/main]), not everything, not just one package |
| CI is slow | Remote cache, affected-only runs, and correct outputs/inputs before adding more machines |
TypeScript in workspaces
- Each package has its own
tsconfig.jsonextending a shared base (packages/tsconfig/base.jsonor roottsconfig.base.json). - With compiled packages, use project references (
"references": [{ "path": "../ui" }],"composite": true) andtsc -bfor incremental builds. - tsconfig
pathsaliases that point into other packages’srcbypass package boundaries and break in production builds; prefer workspace dependencies withexports. "exports"in package.json controls what consumers can import; include atypescondition 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
outputsmeans cache hits restore nothing; missinginputs/envmeans 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 needsfetch-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 diffshows changes only in the owning package’spackage.jsonand 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.