pnpm 12.0
pnpm 12 is stable. It is a rewrite of pnpm in Rust, and it is deliberately not a migration: the commands, flags, settings, and lockfile format of pnpm 11 all carry over, and the documentation describes both versions.
The short list of things that genuinely behave differently is in What's different in pnpm 12. This post covers what pnpm 12 adds that pnpm 11 never shipped.
Installing it
`latest` on npm still points at the pnpm 11 line, so pnpm 12 is installed from the `next-12` tag:
`pnpm self-update next-12`
See Installing pnpm 12 for the other ways, including without Node.js. Homebrew, winget, Scoop, and Chocolatey don't offer it yet.
Breaking changes[](https://pnpm.io/blog/releases/12.0#breaking-changes "Direct link to Breaking changes")
Git dependencies are identities[](https://pnpm.io/blog/releases/12.0#git-dependencies-are-identities "Direct link to Git dependencies are identities")
For repositories on GitHub, GitLab, and Bitbucket, a specifier now names a repository rather than choosing a transport. `github:owner/repo`, `owner/repo`, `git+https://…`, and `git+ssh://git@…` all resolve through the host's canonical HTTPS URL, and the lockfile never records an SSH URL for those hosts. To reach a private hosted repository over SSH, configure the _machine_ with git's own URL rewriting:
`git config --global url."git@github.com:".insteadOf https://github.com/`
pnpm shells out to `git`, so the rewrite applies to all of its git operations. Unknown hosts keep their exact URL, SSH included, and a URL with embedded credentials is kept verbatim and never resolves to a host archive. Details in How git dependencies are resolved.
An unrecognized setting in `pnpm-workspace.yaml` is reported[](https://pnpm.io/blog/releases/12.0#an-unrecognized-setting-in-pnpm-workspaceyaml-is-reported "Direct link to an-unrecognized-setting-in-pnpm-workspaceyaml-is-reported")
A setting pnpm does not recognize used to be ignored in silence — a misspelled `minimumReleaseAge` dropped the policy it was meant to set, and nothing said so. It is now reported, with the closest real setting name suggested when the key looks like a typo.
It **fails** the command with `ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS` when the project pins a pnpm version the running pnpm satisfies: with the pin honored, the setting cannot have been meant for a different pnpm version, so it is a mistake to fix rather than a key to ignore. Everywhere else it is a warning, so a project that has yet to be cleaned up keeps working. The `pnpm config` subcommands never fail on it, so a broken file can still be inspected and repaired.
Lockfiles of cyclic dependency graphs[](https://pnpm.io/blog/releases/12.0#lockfiles-of-cyclic-dependency-graphs "Direct link to Lockfiles of cyclic dependency graphs")
Dependency cycles are now broken canonically during peer resolution: the members of each cycle are ordered by package id, and the edges that close a cycle are always cut at the same place, wherever the installation walks into the cycle from.
The lockfile therefore becomes a pure function of the dependency graph — reordered importers, reordered dependencies, and repeated installs all produce byte-identical lockfiles, which they could not before (#13846, #13865). On large cycle-heavy workspaces peer resolution is 2–3× faster, uses about 25% less memory, and produces a substantially smaller lockfile.
Existing lockfiles keep working: `--frozen-lockfile` consumes them unchanged, and an install that skips resolution leaves them untouched. The first install that actually re-resolves re-keys the walk-order-dependent peer variants of cyclic packages once. See How peers are resolved.
`packageImportMethod: auto` hardlinks first on Linux[](https://pnpm.io/blog/releases/12.0#packageimportmethod-auto-hardlinks-first-on-linux "Direct link to packageimportmethod-auto-hardlinks-first-on-linux")
A reflink materializes a new inode and copies extent bookkeeping inside the filesystem's metadata trees, where a hardlink is one directory entry — on btrfs that roughly halves the time an install spends materializing `node_modules` from a warm store. So on Linux, `auto` now tries the hardlink first.
ext4 is unchanged (cloning was never supported there, so `auto` already hardlinked), and macOS keeps clone-first, where APFS `clonefile` is the platform's cheap primitive. On Linux, cloning becomes the second rung rather than the first, so a store that refuses a hardlink still gets a clone; `packageImportMethod: clone` still asks for one outright.
`engineStrict` follows the edge, not the subtree[](https://pnpm.io/blog/releases/12.0#enginestrict-follows-the-edge-not-the-subtree "Direct link to enginestrict-follows-the-edge-not-the-subtree")
Under `engineStrict`, an install now fails when an incompatible package is reached through a regular `dependencies` edge of an installable package, even when that whole subtree hangs off an `optionalDependencies` entry. pnpm 11 installs the package and emits an install-check warning instead. Packages reachable only through optional edges, or through a package that was itself skipped, are still skipped in both versions (#13286).
New features[](https://pnpm.io/blog/releases/12.0#new-features "Direct link to New features")
Not all of these are exclusive to pnpm 12. **Registry revisions**, the **remote side-effects cache**, **`audit.ignorePrune`**, **batch staged approval**, and the **`pnpm init` latest-tag pin** ship in pnpm 11.25 as well; the rest are v12 only, because they belong to the Rust rewrite.
Project-aware global bins[](https://pnpm.io/blog/releases/12.0#project-aware-global-bins "Direct link to Project-aware global bins")
A globally installed `node`, `deno`, or `bun` follows the version the current project pins, instead of always running the globally installed one — no shell hooks, no `use`-style command. The new `globalShims` setting picks which globally installed packages get such a shim; it defaults to `{ node: true, deno: true, bun: true }` and merges key-wise, so `globalShims: { typescript: true }` adds one without restating the rest.
A stable Node.js release is authenticated against the Node.js release team's signatures and switches without asking. Everything else — Deno, Bun, Node.js prereleases, ordinary package bins you enable — asks _Do you trust this project?_ once per project and per candidate, and remembers the answer machine-locally. `PNPM_SHIM_BYPASS=1` bypasses the feature for one invocation. See Project-aware global bins.
pnpm installs the other package managers[](https://pnpm.io/blog/releases/12.0#pnpm-installs-the-other-package-managers "Direct link to pnpm installs the other package managers")
pnpm now provisions npm, Yarn Classic, Yarn Berry, Yarn 6 (`yarnpkg/zpm`), and Bun, each fetched through the trusted package-manager registries, and each npm-published one verified against npm's signature for its exact version before it runs.
Three things use it. A **git-hosted dependency** is prepared with the package manager _it_ asks for, so a repository built with Yarn installs on a machine that only has pnpm. **`pnx`** runs one for a single command — `pnx yarn@4 install`, `pnx npm@11 ci`, `pnx node@22`. And **`pnpm shim add yarn`** links a `yarn` that runs whatever the current project pins.
Naming a package manager therefore means the tool rather than the npm package that shares its name: `pnpm add -g yarn@4` installs Yarn Berry, and in a project `pnpm add yarn@4` records `"packageManager": "yarn@4.18.0"` — what Corepack reads — while every other package manager is recorded in `devEngines.packageManager`. A specifier that locates a package still installs what it names (`pnpm add yarn@npm:yarn@1.22.22`). See Other package managers.
Registry revisions[](https://pnpm.io/blog/releases/12.0#registry-revisions "Direct link to Registry revisions")
A registry can serve a **replacement artifact** for an already-published version — a rebuild with a vulnerability patched out — without changing the version number and without rewriting the bytes the canonical `name@version` URL has always served. pnpm calls each such artifact a _revision_, addresses it by its complete SHA-512 digest, and records it in the lockfile as one extra line:
pnpm-lock.yaml
`packages: lodash@4.17.21: resolution: integrity: sha512-<replacement-digest> revision: 1`
An entry with no `revision` is revision 0, the original — which is what every entry pnpm has ever written means, so a lockfile that has adopted no replacements is byte-identical to today's.
A dependency or override may pin a revision explicitly as `<version>+rN`, and `pnpm update --patches` refreshes the locked artifacts without changing a single version. pnpr serves revisions for the packages it hosts and proxies them for an upstream registry that advertises them. See Registry revisions.
`pnpm init` pins the latest pnpm[](https://pnpm.io/blog/releases/12.0#pnpm-init-pins-the-latest-pnpm "Direct link to pnpm-init-pins-the-latest-pnpm")
`pnpm init` pins the **latest** released pnpm rather than the version that ran the command, so a project scaffolded by an outdated pnpm no longer inherits that staleness through its own pin (#7490). If the `latest` lookup cannot answer — no network, a slow registry, `offline`, or a `latest` that `minimumReleaseAge` or `trustPolicy` rejects — the running version is pinned as before. The lookup never fails or hangs the command.
Batch approval for staged publishing[](https://pnpm.io/blog/releases/12.0#batch-approval-for-staged-publishing "Direct link to Batch approval for staged publishing")
`pnpm stage approve` approves several staged packages at once. Run it with no stage id to pick from the staged versions interactively, or pass a list. The whole batch is approved with a **single** one-time password, and pnpm asks for a new one only once the registry stops accepting it. Inside a workspace the packages are approved in dependency order, and one whose workspace dependency could not be approved is skipped rather than published against a dependency that never reached the registry.
`audit.ignorePrune`[](https://pnpm.io/blog/releases/12.0#auditignoreprune "Direct link to auditignoreprune")
Set `audit.ignorePrune: true` and `pnpm audit --fix` removes the ignored GHSA entries that no longer appear in the audit report, so a list of tolerated advisories stops accumulating entries for dependencies that are long gone.
Global commands refuse to run under `sudo`[](https://pnpm.io/blog/releases/12.0#global-commands-refuse-to-run-under-sudo "Direct link to global-commands-refuse-to-run-under-sudo")
`pnpm setup`, `pnpm self-update`, and any command that modifies the global installation now fail with `ERR_PNPM_SUDO_NOT_SUPPORTED` under `sudo`, instead of silently operating on the root user's home directory. pnpm keeps global packages and configuration in the invoking user's home, so these commands never need root. Read-only global commands such as `pnpm bin --global` still work.
A remote side-effects cache (proof of concept)[](https://pnpm.io/blog/releases/12.0#a-remote-side-effects-cache-proof-of-concept "Direct link to A remote side-effects cache (proof of concept)")
An opt-in proof of concept lets installs reuse a dependency's build output across machines, by publishing and restoring **signed**, organization-scoped artifacts through pnpr instead of running the lifecycle scripts locally.
A repository names only the eligible `remoteSideEffectsCache.organization` and `packages`; everything describing the act of signing is refused in `pnpm-workspace.yaml` and read from the global config file or the environment instead, so a cloned repository cannot turn the machine's key into a signing oracle. Every cache failure falls back to the ordinary local build. It restores on Linux/glibc x64 and arm64 only for now — see Shared side-effects cache.
Fixes worth knowing about[](https://pnpm.io/blog/releases/12.0#fixes-worth-knowing-about "Direct link to Fixes worth knowing about")
The compatibility database drops its static-analysis entries[](https://pnpm.io/blog/releases/12.0#the-compatibility-database-drops-its-static-analysis-entries "Direct link to The compatibility database drops its static-analysis entries")
The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages a dependent only imports for its _types_, so installing them was at best unnecessary and at worst broke the dependent: `@typescript-eslint/types` gained a `typescript` dependency resolved to the newest release, which put TypeScript 7 under older `@typescript-eslint` versions and made ESLint fail with `Cannot read properties of undefined (reading 'Intrinsic')`. The `@yarnpkg/extensions` entries and pnpm's own curated ones stay.
A store inside the project when nothing above it is linkable[](https://pnpm.io/blog/releases/12.0#a-store-inside-the-project-when-nothing-above-it-is-linkable "Direct link to A store inside the project when nothing above it is linkable")
When no directory above the project accepts a hard link — an AI agent sandbox that grants write access only to the project, or a container with just the project mounted writable — the default store is created at `<project>/node_modules/.pnpm-store` instead of in the pnpm home directory. In those environments the home store is either read-only or on another volume, which forces every package to be copied instead of hard linked (#13525).
The `filterLog` pnpmfile hook is deprecated[](https://pnpm.io/blog/releases/12.0#the-filterlog-pnpmfile-hook-is-deprecated "Direct link to the-filterlog-pnpmfile-hook-is-deprecated")
pnpm 12 ignores `hooks.filterLog` and warns when a pnpmfile defines it. Use `loglevel` to choose how much pnpm reports.
Feedback[](https://pnpm.io/blog/releases/12.0#feedback "Direct link to Feedback")
Please report any issues you run into.