Skip to content

Fix: pnpm Workspace Not Working — workspace:* Protocol, Catalog, Filters, and Hoisting Issues

FixDevs ·

Quick Answer

How to fix pnpm workspace errors — workspace:* not resolving, catalog versions out of sync, --filter not matching, peer deps unmet across packages, shamefully-hoist trade-offs, and publishConfig for releases.

The Error

You add a workspace dependency with workspace:* and pnpm install fails:

ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE 
In packages/app: "@my-org/shared@workspace:*" is in the dependencies but no package named "@my-org/shared" is present in the workspace

Or the package resolves locally but pnpm publish ships a wrong-looking dependency:

{
  "dependencies": {
    "@my-org/shared": "workspace:*"
  }
}

(It should be replaced with a real version on publish.)

Or your --filter pattern matches nothing:

$ pnpm --filter ./apps/web build
No projects matched the filters

Or shared peers like react install at different versions in different packages and tests fail with Invalid hook call:

Invalid hook call. Hooks can only be called inside of the body of a function component.
This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)

Why This Happens

pnpm’s workspace model is stricter than npm’s:

  • Strict node_modules. Each package only sees the dependencies it explicitly declares. This catches accidental imports of transitive deps — and breaks code that relied on them.
  • workspace:* is a protocol, not a version range. It tells pnpm “link to the local copy.” On publish, pnpm rewrites it to the real version of the local package. If that package isn’t in pnpm-workspace.yaml, the protocol can’t resolve.
  • --filter matches against name from package.json and against path globs. Misuse (wrong glob, wrong name pattern) produces “matched nothing” silently.
  • Catalogs unify versions across packages. Without them, each package can pull a different React, leading to “two React copies” runtime bugs.

Fix 1: Declare All Packages in pnpm-workspace.yaml

Every package directory must be covered by the packages field:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "tools/*"

After editing, re-run pnpm install. Verify with:

pnpm list -r --depth -1
# Lists every workspace package the resolver knows about.

If @my-org/shared isn’t listed, the glob didn’t match. Check the directory structure — the name in packages/shared/package.json must be @my-org/shared, and the directory must be under one of the globs.

Common Mistake: Adding a new package directory but forgetting to run pnpm install at the root. The lockfile is stale until you do.

Fix 2: Use workspace:* (or workspace:^) in Internal Dependencies

In apps/web/package.json:

{
  "dependencies": {
    "@my-org/shared": "workspace:*"
  }
}

Three forms:

  • workspace:* — always link to the local version, regardless of what it is.
  • workspace:^ — link to local, publish as ^X.Y.Z (caret of the current local version).
  • workspace:~ — link to local, publish as ~X.Y.Z.
  • workspace:1.2.3 — link to local, publish as 1.2.3 (exact).

For internal apps that don’t publish, workspace:* is simplest. For published libraries that depend on each other, prefer workspace:^ so consumers get semver-compatible releases.

Pro Tip: Use pnpm add @my-org/shared --workspace --filter @my-org/web to add the dependency with the correct workspace: protocol — avoids hand-editing package.json.

Fix 3: Configure publishConfig for Publishable Packages

When you pnpm publish a workspace package, pnpm rewrites workspace: protocols to real versions in the published package.json. But you also want to control which files ship:

{
  "name": "@my-org/shared",
  "version": "1.2.3",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist", "README.md"],
  "publishConfig": {
    "access": "public",
    "main": "./dist/index.js",
    "exports": {
      ".": {
        "import": "./dist/index.mjs",
        "require": "./dist/index.cjs",
        "types": "./dist/index.d.ts"
      }
    }
  }
}

publishConfig fields override the top-level on publish. Useful when:

  • You want different exports paths in published vs source (src/ vs dist/).
  • You need access: "public" for scoped packages on the public npm registry.
  • You’re using module for ESM and want a published consumer-friendly main.

Fix 4: --filter Patterns

--filter accepts package names, paths, and special selectors:

# By package name
pnpm --filter @my-org/web build
pnpm --filter "@my-org/*" build           # All packages in the @my-org scope

# By path
pnpm --filter "./apps/**" build           # All apps
pnpm --filter "./packages/shared" build   # One specific package by path

# By dependents — build a package and everything that depends on it
pnpm --filter ...@my-org/shared build

# By dependencies — build a package and what it depends on
pnpm --filter @my-org/web... build

# Changed since main branch
pnpm --filter "[main]" build

The ... syntax is powerful for incremental rebuilds. To rebuild only what changed since main:

pnpm --filter "...[origin/main]" build

This selects all packages with changes since origin/main plus every package that depends on them, transitively. Cuts build time dramatically in CI.

Common Mistake: Quoting filters wrong on Windows. PowerShell eats the * in --filter @my-org/*. Quote explicitly: pnpm --filter "@my-org/*" build.

Fix 5: Use Catalogs to Pin Shared Versions

Catalogs (added in pnpm 9) let you define versions once at the workspace level and reference them from each package:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

catalog:
  react: ^19.0.0
  react-dom: ^19.0.0
  typescript: ^5.6.0

catalogs:
  testing:
    vitest: ^2.0.0
    "@testing-library/react": ^16.0.0

In each package.json:

{
  "dependencies": {
    "react": "catalog:",
    "react-dom": "catalog:"
  },
  "devDependencies": {
    "vitest": "catalog:testing",
    "@testing-library/react": "catalog:testing"
  }
}

catalog: (no name) refers to the default catalog. catalog:testing refers to a named one. Upgrade React across the entire monorepo by changing one line in pnpm-workspace.yaml.

Note: Catalogs require pnpm 9.0+. On older versions you’ll see Unsupported version reference: catalog:.

Fix 6: Hoisting and Strict Peer Resolution

pnpm puts dependencies in node_modules/.pnpm/ and symlinks only the declared deps into each package’s node_modules/. This is strict-by-default — code that does require("lodash") without declaring lodash will fail.

If a tool you can’t modify needs hoisting:

# .npmrc (at workspace root)
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

This selectively hoists packages matching the patterns into the root node_modules. It’s the safe alternative to shamefully-hoist=true, which dumps everything at the root and defeats the point of pnpm’s strictness.

For React projects with hook-call errors caused by duplicate React installs:

# .npmrc
public-hoist-pattern[]=react
public-hoist-pattern[]=react-dom

Combined with a catalog entry, this guarantees exactly one React in the entire workspace.

Fix 7: Patching Dependencies

When a third-party package has a bug, patch it in place:

pnpm patch some-package
# pnpm creates a temporary copy. Edit files there.
pnpm patch-commit /tmp/abc123
# pnpm writes the patch to patches/ and updates package.json.
{
  "pnpm": {
    "patchedDependencies": {
      "[email protected]": "patches/[email protected]"
    }
  }
}

Commit the patches/ directory. Every pnpm install re-applies the patch.

Pro Tip: Patches are tied to specific versions. When the package upgrades, the patch may not apply. Either pin the version in package.json or be ready to re-create the patch on each upgrade.

Fix 8: CI: Use Frozen Lockfile

In CI, always install with frozen lockfile to catch drift:

# .github/workflows/ci.yml
- run: pnpm install --frozen-lockfile

--frozen-lockfile fails if pnpm-lock.yaml would be modified by the install. This catches:

  • A package.json change not reflected in the lockfile.
  • A lockfile committed without running pnpm install first.
  • A catalog version updated but pnpm install not re-run.

Pair with corepack to pin the pnpm version itself:

{
  "packageManager": "[email protected]"
}

Enable in CI:

- run: corepack enable
- run: pnpm install --frozen-lockfile

Still Not Working?

A few less-obvious failures:

  • ENOENT: no such file or directory for a workspace package. The package was added to pnpm-workspace.yaml but its directory doesn’t exist yet. Create the directory with a minimal package.json before installing.
  • workspace:* shipped to npm un-rewritten. You published with npm publish instead of pnpm publish. Only pnpm publish rewrites the protocol. Use it (or wire up Changesets / Release Please which use it internally).
  • Two copies of react after install. Either the catalog isn’t applied (check pnpm-workspace.yaml), or one package declares react as a peer dep and ships its own dev version. Run pnpm why react to trace.
  • pnpm install slow in CI even with cache. Cache the ~/.local/share/pnpm/store directory. actions/setup-node doesn’t cache it by default — use pnpm/action-setup or set up caching manually.
  • tsc -b rebuilds everything despite Turborepo cache. TypeScript references invalidate on package.json changes. Either narrow the deps that affect the cache key, or use tsc --build --incremental with cached tsbuildinfo files.
  • pnpm install deletes node_modules of a package. A prepare script in that package fails, pnpm rolls back. Check the package’s prepare/postinstall script for errors.
  • --filter includes the root package unexpectedly. The workspace root counts as a package with name from root package.json. Filter it out with --filter !@root-name if it’s interfering.
  • PowerShell on Windows: pnpm exec doesn’t find local binaries. Use pnpm dlx for one-off runs, or add the local node_modules/.bin to PATH inside the command.

For related package manager and monorepo issues, see pnpm peer dependency error, Turborepo not working, npm eresolve unable to resolve dependency tree, and npm err peer dep conflict.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles