Monorepo Tooling skill for Codex

Cache hits you can trust, CI that builds only what changed. Codex picks between plain pnpm workspaces, Turborepo and Nx based on repo size and needs, then declares each task's dependencies, inputs, outputs and environment variables so caching is both fast and correct. It proves the setup with a fully cached second run, a single-package change that rebuilds only dependents, and an env var change that forces a miss, and wires affected-only CI with full git history.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- monorepo-tooling

Installs to ~/.agents/skills/monorepo-tooling/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $monorepo-tooling, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Sets up and debugs JavaScript and TypeScript monorepo task running with pnpm workspaces, Turborepo and Nx - workspace protocol and catalogs, task graphs with dependsOn, declared inputs, outputs and environment variables so caching is correct, affected-only CI with full git history, remote caching and module boundaries. Use when creating or restructuring a monorepo, choosing between Turborepo and Nx, fixing stale or missing cache hits, making CI build only changed packages, or adding a new app or package to an existing workspace.

Use it when

  • Creating a monorepo or adding apps and packages to one
  • Builds are cached wrongly, restored empty, or never hit
  • CI runs every task in every package on each PR
  • Choosing between Turborepo, Nx and plain workspaces

Use something else when

  • You need repo-level ownership and review routing per package: use codeowners-setup
  • You are speeding up a single-package CI pipeline: use ci-speedup
  • You are deciding how to split code into services or modules: use modular-monolith

How to check the result

A second full run is entirely cache hits, touching one leaf package rebuilds only it and its dependents, and changing a declared environment variable causes a cache miss for the tasks that use it.

The mistake it prevents

Leaving a build-time environment variable out of the task's env list, so a cached artifact built with another environment's value gets deployed.

~/.agents/skills/monorepo-tooling/SKILL.md

--- name: monorepo-tooling description: Sets up and debugs JavaScript and TypeScript monorepo task running with pnpm workspaces, Turborepo and Nx - workspace protocol and catalogs, task… license: MIT ---

Done well means: every task declares what it depends on, which files and environment variables change its result, and which files it produces. Then a second run with no changes is fully cached, a change in one package rebuilds only it and its dependents, and no cache hit ever serves an artifact built with different inputs. CI runs only affected work and stays correct.

Inspect

cat pnpm-workspace.yaml package.json turbo.json nx.json 2>/dev/null | head -120
ls apps packages libs 2>/dev/null
git ls-files '*package.json' | grep -v node_modules | wc -l

Read AGENTS.md for package manager and CI commands. Note the lockfile in use (only one should exist) and how internal packages are consumed: compiled (dist), or TS source via exports.

Choose the tool

SituationUse
Up to a handful of packages, simple scriptspnpm workspaces alone: pnpm -r, --filter
JS/TS repo that wants caching and a task graph with minimal configpnpm workspaces + Turborepo
Large org, many teams, generators, enforced boundaries, polyglot pluginsNx (on pnpm/npm/yarn workspaces)
Already on one of theseStay; fix its configuration rather than migrating

pnpm workspaces

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
catalog:
  react: ^19.1.0
  typescript: ^5.9.0
// apps/web/package.json
{ "dependencies": { "@acme/ui": "workspace:*", "react": "catalog:" } }
pnpm install --frozen-lockfile                 # CI
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          # packages changed since main, plus dependents
pnpm add -D -w typescript                      # root dev dependency
pnpm why react                                 # duplicate versions

workspace:* is rewritten to the real version on publish. Catalogs keep one version of a shared dependency across packages.

Turborepo

{
  "$schema": "./node_modules/turbo/schema.json",
  "globalDependencies": [".env.shared", "tsconfig.base.json"],
  "globalEnv": ["NODE_ENV"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", "!**/*.test.*", "!**/*.md"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NEXT_PUBLIC_*", "API_BASE_URL"]
    },
    "typecheck": { "dependsOn": ["^build"] },
    "lint": {},
    "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] },
    "dev": { "cache": false, "persistent": true }
  }
}
  • ^build means “build of my dependencies first”; build without the caret means a task in the same package; @acme/ui#build targets one package’s task.
  • outputs is what gets restored on a cache hit. Missing outputs give a “cache hit” that restores nothing and a broken downstream build.
  • inputs defaults to all git-tracked files in the package; $TURBO_DEFAULT$ keeps that default while you add exclusions.
  • Environment variables that affect output must be in env (task) or globalEnv. In strict env mode (the default in Turborepo 2), variables not listed are also hidden from the task at runtime; runtime-only variables that should not affect the hash go in passThroughEnv.
turbo run build                                  # whole graph
turbo run build --filter=@acme/web...            # web plus its dependencies
turbo run test lint --affected                   # changed packages vs the default branch, plus dependents
turbo run build --dry=json | jq '.tasks[] | {taskId, cache: .cache.status}'
turbo run build --summarize                      # writes a run summary under .turbo/runs/
turbo run build --force                          # ignore cache (debugging only)

Remote cache: turbo login and turbo link for Vercel’s cache, or set TURBO_API, TURBO_TOKEN and TURBO_TEAM for a self-hosted cache server. Linking an account or writing tokens is an account change: ask the user first, and keep tokens in CI secrets.

Nx

{
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/**/*.md"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production", { "env": "API_BASE_URL" }],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    },
    "test": { "inputs": ["default", "^production"], "cache": true }
  }
}
npx nx graph                                         # interactive project graph
npx nx show project web                              # resolved targets, including inferred ones
npx nx affected -t lint test build --base=origin/main --head=HEAD
npx nx run-many -t build --projects=tag:scope:shop
npx nx reset                                         # clear local cache and daemon

Nx plugins infer targets from tool configs (Vite, Jest, Next.js); check nx show project before adding explicit targets that duplicate them. Enforce dependency direction with the @nx/enforce-module-boundaries ESLint rule and project tags (type:ui may not import type:feature). Remote caching is Nx Cloud (nx connect, an account action that needs approval) or a self-hosted cache.

CI

- uses: actions/checkout@v4
  with:
    fetch-depth: 0            # affected detection needs the merge base
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
  with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint typecheck test build --affected

Without a remote cache, persist .turbo/cache or .nx/cache with actions/cache keyed on the lockfile hash and the commit SHA. For Nx on GitHub Actions, nrwl/nx-set-shas computes the last successful base commit for affected.

Adding a package checklist

- [ ] Created under apps/ or packages/ matching the workspace globs
- [ ] name scoped (@acme/x), "private": true unless published
- [ ] Internal deps use workspace:*; shared versions use catalog:
- [ ] Scripts named like the rest (build, test, lint, typecheck)
- [ ] Outputs match the task's outputs globs; tsconfig extends the base
- [ ] Tags / boundaries set (Nx); CODEOWNERS entry added
- [ ] pnpm install updated the lockfile; affected run picks the package up

Gotchas

  • An env var baked into a build (API URL, feature flag) but missing from env produces a cache hit with the wrong value in production. Audit process.env. usage per package.
  • Tasks that write outside their package directory, or read files not under it, break hashing. Move shared inputs into globalDependencies / sharedGlobals.
  • Non-deterministic outputs (timestamps, absolute paths) defeat remote cache sharing.
  • Circular dependencies between packages make ^build impossible; pnpm ls -r --depth -1 plus nx graph or turbo run build --graph reveals them. Break the cycle by extracting the shared part.
  • Phantom dependencies: code importing a package it does not list works with hoisting and fails elsewhere. pnpm’s isolated layout surfaces these; fix by declaring the dependency.
  • Shallow clones make --affected either fail or treat everything as changed.
  • Cached logs replay on hits; never print secrets in build output.

Verify

  1. Run the full task twice; the second run must be entirely cache hits.
  2. Touch one leaf package’s source; turbo run build --affected --dry=json or nx show projects --affected lists only it and its dependents.
  3. Change a declared env var; the affected task must miss the cache.
  4. Delete outputs locally and rerun; the cache must restore them.

Report: tool and version, task graph summary, cache hit rate before and after, CI time before and after, and any undeclared env vars or outputs found.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Monorepo Tooling"
  short_description: "Cache hits you can trust, CI that builds only what changed"
  default_prompt: "$monorepo-tooling Set up Turborepo in our pnpm monorepo so CI only builds and tests what changed"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem