Cached at:
08/27/26, 06:17 AM
# pnpm 12.0
Source: [https://pnpm.io/blog/releases/12.0](https://pnpm.io/blog/releases/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](https://pnpm.io/motivation)describes both versions\.
The short list of things that genuinely behave differently is in[What's different in pnpm 12](https://pnpm.io/blog/whats-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:
See[Installing pnpm 12](https://pnpm.io/installation#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)
### Git dependencies are identities[](https://pnpm.io/blog/releases/12.0#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."
[email protected]:".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](https://pnpm.io/package-sources#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)
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)
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](https://github.com/pnpm/pnpm/issues/13846),[\#13865](https://github.com/pnpm/pnpm/issues/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](https://pnpm.io/how-peers-are-resolved#cyclic-dependencies)\.
### `packageImportMethod: auto`hardlinks first on Linux[](https://pnpm.io/blog/releases/12.0#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`](https://pnpm.io/settings/node-modules#what-auto-tries-first)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)
Under[`engineStrict`](https://pnpm.io/settings/cli#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](https://github.com/pnpm/pnpm/issues/13286)\)\.
## New features[](https://pnpm.io/blog/releases/12.0#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)
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`](https://pnpm.io/settings/other#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](https://pnpm.io/global-packages#project-aware-global-bins)\.
### pnpm installs the other package managers[](https://pnpm.io/blog/releases/12.0#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`](https://pnpm.io/cli/pnx)**runs one for a single command —`pnx yarn@4 install`,`pnx npm@11 ci`,`pnx node@22`\. And**[`pnpm shim add yarn`](https://pnpm.io/cli/shim)**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](https://pnpm.io/package-managers)\.
### Registry revisions[](https://pnpm.io/blog/releases/12.0#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:
[email protected]: 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`](https://pnpm.io/cli/update#--patches)refreshes the locked artifacts without changing a single version\.[pnpr](https://pnpm.io/pnpr)serves revisions for the packages it hosts and proxies them for an upstream registry that advertises them\. See[Registry revisions](https://pnpm.io/registry-revisions)\.
### `pnpm init`pins the latest pnpm[](https://pnpm.io/blog/releases/12.0#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](https://github.com/pnpm/pnpm/issues/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)
[`pnpm stage approve`](https://pnpm.io/cli/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)
Set[`audit\.ignorePrune: true`](https://pnpm.io/cli/audit#auditignoreprune)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)
`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)
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](https://pnpm.io/pnpr)instead of running the lifecycle scripts locally\.
A repository names only the eligible[`remoteSideEffectsCache\.organization`and`packages`](https://pnpm.io/settings/build#remotesideeffectscache); 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](https://pnpm.io/pnpr/shared-side-effects-cache)\.
## Fixes worth knowing about[](https://pnpm.io/blog/releases/12.0#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)
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)
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](https://github.com/pnpm/pnpm/issues/13525)\)\.
### The`filterLog`pnpmfile hook is deprecated[](https://pnpm.io/blog/releases/12.0#the-filterlog-pnpmfile-hook-is-deprecated)
pnpm 12 ignores[`hooks\.filterLog`](https://pnpm.io/pnpmfile#hooksfilterlog)and warns when a pnpmfile defines it\. Use[`loglevel`](https://pnpm.io/settings/cli#loglevel)to choose how much pnpm reports\.
## Feedback[](https://pnpm.io/blog/releases/12.0#feedback)
Please[report any issues](https://github.com/pnpm/pnpm/issues)you run into\.