Use when configuring a pnpm-workspaces monorepo, debugging hoisting/peer-dep resolution, adopting the catalog protocol, integrating with Turborepo or Nx, fixing "cannot find module" issues that only happen in CI, or migrating from npm/yarn workspaces. Triggers: pnpm-workspace.yaml setup, packageExtensions for broken peer deps, .npmrc tuning (public-hoist-pattern, shamefully-hoist), workspace protocol (workspace:*), filtering with --filter, version mismatches across packages, "Cannot find module \"foo\"" only after lockfile update, ESM/CJS interop in workspace packages. NOT for npm/yarn-specific workspace bugs, monorepos using Bazel/Lerna/Rush, or single-package projects.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use when configuring a pnpm-workspaces monorepo, debugging hoisting/peer-dep resolution, adopting the catalog protocol, integrating with Turborepo or Nx, fixing "cannot find module" issues that only happen in CI, or migrating from npm/yarn workspaces. Triggers: pnpm-workspace.yaml setup, packageExtensions for broken peer deps, .npmrc tuning (public-hoist-pattern, shamefully-hoist), workspace protocol (workspace:*), filtering with --filter, version mismatches across packages, "Cannot find module \"foo\"" only after lockfile update, ESM/CJS interop in workspace packages. NOT for npm/yarn-specific workspace bugs, monorepos using Bazel/Lerna/Rush, or single-package projects.
metadata
{"category":"DevOps & Infrastructure","tags":["pnpm","monorepo","workspaces","turbo","dependencies","typescript"],"provenance":{"kind":"first-party","owners":["port-daddy"]},"pairs-with":[{"skill":"ideal-web-app-builder","reason":"New TS web apps scaffolded there typically live in a pnpm workspace; this skill owns the workspace/catalog/turbo layer around them"},{"skill":"git-best-practices","reason":"Affected-only CI (--filter \"...[origin/main]\") and turbo cache keys depend on the disciplined branch/merge hygiene that skill owns"}],"io-contract":{"kind":"deliverable","consumes":["[Truncated]","[Truncated]"],"produces":["[Truncated]","[Truncated]"]}}
pnpm Workspace Monorepo
pnpm's strict resolution catches dependency mistakes that npm and yarn silently allow. That's its biggest feature and its biggest source of confusion. Most "weird CI failure" stories in pnpm monorepos boil down to a missing peer-dep declaration that npm-style hoisting was hiding.
When to use
Standing up a new monorepo or migrating from npm/yarn workspaces.
A package builds locally but fails CI with "cannot find module".
Bumping a version and watching half the workspace break.
Centralizing common dep versions (catalog protocol).
Wiring Turborepo / Nx for caching.
Debugging peer-dependency warnings that look harmless but break runtime.
# pnpm-workspace.yamlpackages:-'packages/*'-'apps/*'# Catalog protocol — pinned versions referenced by name.catalog:react:^19.0.0typescript:5.7.2zod:^3.25.0# Multiple named catalogs for divergent stacks.catalogs:legacy:react:^18.2.0
In each package:
{"name":"@myorg/ui","dependencies":{"@myorg/core"
:
"workspace:*"
,
"react"
:
"catalog:"
,
"zod"
:
"catalog:"
}
,
"devDependencies"
:
{
"@myorg/tsconfig"
:
"workspace:*"
,
"typescript"
:
"catalog:"
}
}
workspace:* resolves to the local package on install and to a real version range when published. catalog: resolves to whatever the workspace catalog pins. One source of truth, no version drift.
Filtering
# Run a script in one package.
pnpm --filter @myorg/api dev
# In a package and everything that depends on it.
pnpm --filter @myorg/core... build
# In a package and everything it depends on.
pnpm --filter ...@myorg/api build
# Changes since main.
pnpm --filter "...[origin/main]"test# Multiple filters.
pnpm --filter "@myorg/api" --filter "@myorg/marketing" build
The dot ellipses are dependency direction: pkg... includes downstream consumers; ...pkg includes upstream deps.
.npmrc knobs that matter
# Strict by default — pnpm doesn't hoist. Tools that assume hoisting break.# Allow specific things to hoist when a tool needs them at the root.
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=@types/*
# When a dep doesn't declare a peer correctly and patching upstream isn't viable.auto-install-peers=true# Catalog mode: 'strict' fails the install if a workspace package uses a non-catalog version.# Encourage the catalog without forcing it during migration.manage-package-manager-versions=true# Avoid lifecycle scripts running in CI for postinstalled deps you don't trust.side-effects-cache-readonly=true
shamefully-hoist=true is a last resort. It defeats pnpm's strictness and reintroduces npm-style implicit deps.
packageExtensions — fix broken peer deps without forking
This is a non-disruptive way to patch package.json for installed deps. Write the issue link as a comment so future-you knows to remove it after upstream fixes.
--frozen-lockfile fails CI if pnpm-lock.yaml would change. Always use it in CI.
Anti-patterns
Implicit transitive dep
Symptom: App imports lodash (which it doesn't declare) but the build works locally because some other dep transitively pulled it in.
Diagnosis: pnpm's strict resolution hides transitive deps from your code; this works on npm. Until pnpm.
Fix: Declare every dep you import. pnpm why lodash shows who's actually pulling it in.
workspace:* accidentally published
Symptom: Consumer of a published package gets workspace:* in node_modules and chokes.
Diagnosis: Forgot to convert workspace:* → real semver on publish.
Fix: Use pnpm publish (handles the rewrite) or pnpm pack to inspect first. Never run npm publish directly in a pnpm monorepo.
shamefully-hoist=true to "fix" a missing peer
Symptom: App works locally with shamefully-hoist; CI fails when hoisting differs.
Diagnosis: Hiding a real bug under hoisting. Some tool needs a peer that's not declared.
Fix: Find the missing peer, add it to the package's peerDependencies or use packageExtensions. Remove shamefully-hoist.
Turbo cache invalidating on every commit
Symptom: No turbo cache hits despite config looking right.
Diagnosis:inputs not declared — turbo defaults to "everything in the package," so an unrelated file change busts the cache.
Fix: Set inputs per task: ["src/**/*.{ts,tsx}", "package.json", "tsconfig.json"].
Catalog drift via direct edits
Symptom: Half the workspace is on react@^19, half on ^18.
Diagnosis: Someone bumped react in one package's package.json instead of in the catalog.
Fix: Adopt catalog: for shared deps. Set pnpm.overrides for transitive single-version enforcement. Lint with manage-package-manager-versions.
Lifecycle scripts running with secrets in env
Symptom: A postinstall in a transitive dep prints sensitive env vars (or worse).
Diagnosis: pnpm runs lifecycle scripts by default.
Fix:pnpm install --ignore-scripts for installs in CI sensitive paths; allowlist scripts via pnpm.onlyBuiltDependencies.
Quality gates
pnpm-lock.yaml committed; --frozen-lockfile enforced in CI.
No workspace:* ranges in published artifacts.
Shared dep versions live in the catalog (catalog: protocol).
No shamefully-hoist=true; missing peers fixed via packageExtensions or proper declarations.
Turbo task inputs declared so the cache reflects real dependencies.
CI uses --filter "...[origin/main]" so only affected packages build.
Every package has engines.node matching the repo Node version.
pnpm.onlyBuiltDependencies allowlist for postinstall scripts.
Deterministic Audit
Before adopting (or reviewing) a workspace configuration, write it as a JSON plan matching
schemas/pnpm-workspace-monorepo-plan.schema.json and run it through the deterministic
auditor:
auditPnpmWorkspaceMonorepo(plan) (in scripts/pnpm_workspace_monorepo_audit.mjs) turns this
skill's Anti-patterns and Quality Gates into machine-checkable rules over structured fields —
no keyword matching: shamefully-hoist in any form (including as a "fix" for a missing peer),
raw npm publish in a pnpm workspace (the workspace:* leak), a missing or non-frozen
lockfile in CI, internal deps not on the workspace: protocol, shared versions drifting
per-package instead of living in the catalog, turbo tasks without declared inputs (the
cache-busts-every-commit failure), unfiltered CI builds, TS project references that do not
mirror the dependency graph, and an open postinstall surface with no
onlyBuiltDependencies allowlist. It returns { pass, score, findings, recommendations } so
a reviewer or CI gate can reject a drift-prone setup without re-deriving the reasoning.
examples/sample-input.json is a catalog + turbo + frozen-lockfile plan that audits
pass: true. Version history lives in CHANGELOG.md.
NOT for
npm or yarn workspaces — different resolution semantics.
Bazel / Lerna / Rush — different orchestration models.
Single-package repos — pnpm works fine, but workspace features don't apply.
Bun workspaces — Bun's workspace support is similar but has its own quirks. No dedicated skill.
CI matrix design over the workspace packages — once you have working pnpm --filter ..., the matrix shape is upstream. → github-actions-matrix-patterns.