The workspace: protocol in pnpm tells the resolver that a dependency must come from your own monorepo. If a matching package is not there, installation fails. Without it, pnpm can fall back to the public registry, which is the situation where an unrelated package with the same name could be installed.
What the protocol looks like
In a package.json inside a pnpm workspace:
"foo": "workspace:2.0.0"
The pnpm documentation says that with this spec, installation fails if foo@2.0.0 is not in the workspace, instead of falling back to the registry. Aliases work as well, for example "bar": "workspace:foo@*".
The workspace itself is defined in pnpm-workspace.yaml:
packages:
- packages/*
Why a silent fallback is risky
Say you rename an internal package from acme-utils to acme-core and forget one consumer. With a plain range like ^1.0.0, the resolver may look at the registry. If someone has published a package called acme-utils there, you now run code you did not write. This is the same shape as a dependency confusion attack. For the background, see what a dependency confusion attack is.
With workspace:, the rename causes a clear error on the next install. A failed install is cheap. A wrong package in a build is not.
The linkWorkspacePackages setting
pnpm also has a linkWorkspacePackages setting, default false, with values true, false and deep. The docs say true links a workspace project only where a project of the workspace declares it directly, and deep also links workspace projects into subdependencies. With the default of false, a plain version range is resolved normally unless you use the workspace: protocol. That is the reason to be explicit.
What happens at publish time
When you run pnpm pack or pnpm publish, pnpm rewrites the protocol using the target package's version:
| In the repo | In the published package |
|---|---|
workspace:* | 1.5.0 |
workspace:~ | ~1.5.0 |
workspace:^ | ^1.5.0 |
workspace:^1.5.0 | ^1.5.0 |
A bare workspace: is treated as workspace:*. Consumers outside your repo see ordinary versions.
A short checklist
- Use workspace: for every dependency that is meant to be internal.
- Keep the shared lockfile at the workspace root. The docs say sharedWorkspaceLockfile defaults to true, which creates a single pnpm-lock.yaml there.
- Reserve a scope for internal package names, and publish only under it.
- Review renames and removals of internal packages as dependency changes, not as refactors.
Safeguard dependency and SBOM scanning can list the packages in each part of a repository and flag known vulnerabilities. You can try a free scan at gold.safeguard.sh/scan.
Quick check
Does workspace: change what gets published?
Yes, in a good way. pnpm replaces it with a real version range when packing or publishing.
Why not just rely on a version range?
A range can be satisfied from the registry. The protocol makes an internal dependency fail if the internal package is missing.
Sources
Last reviewed: 19 October 2026.