NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #466 most downloaded on npm
Fast, disk space efficient package manager
Last release today
02 Oct 2026
Ships on a steady schedule
a new release about every 8 days
Nearly every release is documented
notes for 56 of the last 60 stable releases
376 versions withdrawn
withdrawn after publishing
14 years old
1336 releases · first in 2013
Breaking change from pnpm v11. Under engineStrict, an install fails when an incompatible package is reached through a regular dependencies edge of an…
The Rust engine now reads four more settings from pnpm-workspace.yaml and PNPM_CONFIG_*, instead of only accepting them as CLI flags:
frozenLockfile — pnpm install grows a --no-frozen-lockfile flag so the setting can be overridden in both directions. As in pnpm, it cannot be set in the global config.yaml.savePrefix — the range operator pnpm add saves, still overridable with --save-prefix / --save-exact.savePeer — pnpm add also records the new dependency in peerDependencies. pnpm add --no-save-peer overrides it back off.saveCatalogName — the catalog pnpm add saves into.The Rust engine now supports the saveWorkspaceProtocol setting, so pnpm add <pkg>@workspace:… writes back the same specifier pnpm does. Under the default rolling, a request like workspace:^1.2.3 is saved as workspace:^ — a range with no version in it, so bumping the workspace package never has to touch its dependents' manifests. saveWorkspaceProtocol: true saves the workspace package's resolved version instead (workspace:^2.5.0), and false keeps the workspace: form only when it was asked for explicitly. Previously the specifier was written back exactly as typed.
pnpm update --workspace is supported: dependencies that a workspace project publishes are re-pointed at the local copies through the workspace: protocol. The saveWorkspaceProtocol setting is honored — under its rolling default an entry becomes workspace:*, workspace:^, or workspace:~ (whichever matches the range it already declared), so a sibling's next release does not invalidate it. Naming a dependency that is not in the workspace fails with ERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUND, and combining the flag with --latest fails with ERR_PNPM_BAD_OPTIONS.
pnpm update --depth <number> is now applied per dependency instead of only distinguishing 0 from higher values: a dependency deeper than the given depth keeps its locked resolution, so pnpm update --depth 0 updates direct dependencies only.
pnpm run "/^build:(backend|frontend)$/" selects every script whose name matches the pattern, in single-project and recursive runs alike #13322. Flags on the selector are rejected with ERR_PNPM_UNSUPPORTED_SCRIPT_COMMAND_FORMAT, as pnpm does.
pnpm self-update no longer takes any instruction from the project it is run in:
.npmrc or pnpm-workspace.yaml can no longer redirect the download or attach credentials to it, and the project's default .pnpmfile.(c|m)js is no longer loaded. Pnpmfiles from trusted sources (the pnpmfile setting, the global pnpmfile, config dependencies) still apply.minimumReleaseAge settings in pnpm-workspace.yaml no longer affect self-update. They still govern the project's own dependencies; for self-update the cooldown now comes from the built-in default, your global config, a PNPM_CONFIG_* environment variable, or a command-line flag. This fixes self-update failing inside a workspace that raises the cutoff while succeeding everywhere else, and stops a repository from either waiving the cooldown or keeping you on an outdated pnpm by raising it.trustPolicy settings and to ci: a project can no longer weaken the trust check that guards the pnpm download, nor re-enable the confirmation prompt that a CI run suppresses.When self-update refuses a version that is younger than the cutoff, an interactive run now offers to update anyway; non-interactive runs still fail. CI never prompts, even on a runner that attaches a TTY.
An aliased dependency of a protocol that resolves under its own package name — jsr: and the named registries — is recorded in the lockfile importer again. "bar-from-jsr": "jsr:@pnpm-e2e/bar@1.0.0" resolved and installed, but the importer stayed empty, so nothing reading direct dependencies out of the lockfile (outdated, update, licenses, dedupe, frozen-install verification) could see it #13362.
An allowBuilds entry with the set this to true or false placeholder pnpm scaffolds no longer makes every command in that workspace fail with a config-parse error #13322. An undecided entry now leaves the package under the default-deny build policy, as pnpm does.
Two pnpm install resolution fixes that made large workspaces such as Astro produce a different pnpm-lock.yaml than pnpm 11 #13334:
file: protocol ("@test/pkg": "file:./pkg") is recorded as a link: again instead of being copied in as a file: snapshot.bundledDependencies / bundleDependencies are no longer resolved as dependencies of their own. npm ships them inside the package's tarball, so installing them again added packages the lockfile should not contain (for example napi-wasm under @parcel/watcher-wasm).Executables that a package ships inside its own tarball (bundledDependencies) are linked again into that package's node_modules/.bin, under both the isolated and the hoisted node linker. A package that declares bundleDependencies: true instead of a list of names is now recorded in pnpm-lock.yaml the way pnpm 11 records it, and such a lockfile can be read back.
Aligned pnpm licenses list --json package metadata and license-group ordering with the TypeScript CLI.
Fixed pnpm --filter <package> run to list the selected package's scripts and root workspace scripts when no script name is specified.
Strip Unicode formatting characters from registry- and manifest-derived terminal output.
Prevented dependency verification before scripts from rewriting an up-to-date lockfile.
Concurrent commands in a repository that pins packageManager no longer race while installing the pinned pnpm version on a cold cache #13322. A task runner spawning several pnpm run children at once could previously fail with "failed to remove existing directory … prior to swap", or leave a child looking for a binary another process had just unlinked.
Config-load warnings, such as the warning about install settings left under the pnpm field of package.json, are printed to stderr instead of stdout #13361.
pnpm dedupe --check now reports what deduplication would change: the importer and package snapshot diff, the ERR_PNPM_DEDUPE_CHECK_ISSUES error code, and the warning that points at pnpm peers check when the install leaves peer-dependency issues behind. pnpm peers check is also accepted again — the subcommand spelling used on pnpm.io and in pnpm's own dedupe output — instead of failing with "unexpected argument 'check' found" #13321.
A deprecated package is reported once rather than once per workspace project that depends on it, and is no longer double-counted in the "deprecated subdependencies found" summary when it is also a direct dependency #13322. Ignored build scripts are also listed with their (patch_hash=…) suffix, so two copies of a package that differ only by an applied patch are distinguishable.
pnpm install --frozen-lockfile no longer re-imports a varying subset of packages on every repeat install of an unchanged project #13316. The global-virtual-store directory of a package that takes part in a dependency cycle was derived from an order that changed from run to run, so those packages landed on a fresh slot each time; it is now derived deterministically and matches the directory pnpm itself computes.
When the pinned packageManager engine install cannot take its lock because the store cannot be written to, pnpm now reports that instead of quietly installing without the lock. A lock another process holds is unchanged — it is still waited for.
Speed up installs after compatible catalog or direct dependency range changes by retaining the locked version without resolving the dependency graph again.
Speed up installs after safe override changes by reusing unambiguous compatible dependency resolutions, pruning obsolete dependencies, applying independent replacements and removals together, and handling parent-scoped "-" overrides without full lockfile resolution.
Fixed warm side-effects cache reuse for git dependencies.
Aligned pnpm dedupe --check progress and error output with the TypeScript CLI.
A frozen install now fails when autoInstallPeers, dedupePeers, or excludeLinksFromLockfile has changed since pnpm-lock.yaml was written, instead of installing against a lockfile that no longer matches the settings. The error names the drifted setting, as pnpm install --frozen-lockfile has always done.
A repeat pnpm install --frozen-lockfile is a no-op again when the project has a platform-incompatible optional dependency. The skipped package is kept in node_modules/.pnpm/lock.yaml (.modules.yaml is what records the skip), so the install can once more recognize an unchanged tree instead of re-running every lifecycle and dependency build script #13312.
Reject frozen installs when the current pnpmfile does not match the lockfile's pnpmfileChecksum.
Fixed pnpm licenses list to report dependencies from every workspace project, exclude unsupported platform packages, and mark development dependencies.
Setting both autoInstallPeers: false and dedupePeerDependents: false now leaves missing peers alone, instead of still installing the ones a version elsewhere in the workspace could satisfy.
Installing a local file: directory dependency with the global virtual store enabled no longer fails with TypeError: Cannot read properties of undefined (reading 'split') #13335.
Local directory dependencies — file: directories and injected workspace packages — now get a global-virtual-store slot of their own per project. They used to share one slot across every project that depended on a directory of the same name, so a project could end up linked to another project's copy of the dependency.
A missing required peer is no longer auto-installed as a prerelease that its declared range rejects. A package peer-depending on ^29.0.0 || ^30.0.0 next to a 30.0.0-alpha.6 pulled in elsewhere in the graph now resolves a stable 29.x/30.x from the registry instead of adopting the alpha #13341.
A lockfile entry for a git-hosted archive that records no integrity installs again instead of failing with ERR_PNPM_MISSING_TARBALL_INTEGRITY. Older pnpm versions wrote that shape for dependencies like "ci-info": "watson/ci-info#f43f6a1c…", so any committed lockfile still carrying one could not be installed #13308. The archive URL pins a full commit SHA, and pnpm fetches it without an integrity check.
Every other remote tarball still has to carry an integrity, and the refusal now points at the repair: pnpm clean --lockfile followed by pnpm install.
Error output no longer repeats the same message once per level of the internal error chain.
The Workspace column of pnpm update --interactive now falls back to the project's path when its name is only whitespace, as it already did for a missing or empty one — all three render an equally blank label otherwise.
pnpm update --interactive now groups the dependencies it offers by dependency type — dependencies, devDependencies, optionalDependencies, peerDependencies, and GitHub Actions each get their own heading — and lays each group out as a column-aligned table with a Package/Current/Target/URL header, instead of one flat list.
pnpm update --interactive now measures its table in terminal columns rather than in characters. A package name, workspace name, or version containing wide characters (CJK, most emoji) no longer knocks its row's columns out of line with the rest of the group, and a wide character in a version no longer aborts the command with Subject parameter value width cannot be greater than the container width #13357.
pnpm update --interactive run inside a workspace now shows a Workspace column naming the project each outdated dependency was found in, so the same package outdated in several projects can be told apart.
Write single-value libc package metadata in the same scalar form as pnpm.
Fixed pnpm install silently skipping a local file:*.tgz dependency: the package is now extracted into the virtual store, recorded under packages: and snapshots:, and linked into node_modules #13379.
A frozen install whose recorded settings no longer match the configuration — overrides, catalogs, patchedDependencies, and the rest — now fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH naming the one setting that changed, instead of ERR_PNPM_OUTDATED_LOCKFILE with the whole map dumped #13322.
Fixed pnpm install dropping a package that ships no package.json of its own from the lockfile. Such a package is now named after its alias and recorded at version 0.0.0 under packages: and snapshots:, and its extraction gets the placeholder package.json pnpm writes #13410.
Resolve optional peers from versions provided by local workspace packages, omit empty deprecation messages from generated lockfiles, and preserve valid lockfile pins in pnpm dedupe --check.
Aligned license reports, dedupe --check progress and spacing, and dependency-verification output with pnpm.
A file: dependency declared by a package that was itself installed from a local directory is now resolved relative to that package's directory, not to the importer's #13323. Installing a project whose local dependency depends on a sibling directory (file:../child) no longer fails with Could not install from "…" as it does not exist, and the snapshot entry for such a dependency is now written as file:<path> instead of <name>@file:<path>, matching the lockfile pnpm writes.
npm_config_user_agent now carries the configured user agent (pnpm/<version> …) in install lifecycle scripts, pnpm run, pnpm exec, and pnpm dlx #13322. It was previously unset for install scripts and the bare string pnpm elsewhere, which made preinstall guards that check for pnpm reject the install.
An auto-installed optional peer is no longer hoisted at a version the workspace root's own dependency on that package excludes. resolvePeersFromWorkspaceRoot already made the workspace root's specifier decide which version a missing required peer is installed at; the optional-peer picker ignored it and always took the highest version present anywhere in the graph. In a workspace whose root pins postcss: 8.5.10, an importer that depends on webpack and declares no postcss of its own got postcss@8.5.22 hoisted for terser-webpack-plugin's optional postcss peer, leaving two postcss@8.5.x instances in the graph #13320.
A missing optional peer dependency is no longer satisfied by a prerelease version that its declared range doesn't accept. ts-jest, which declares @jest/transform and jest-util as optional peers with ^29.0.0 || ^30.0.0, was bound to 30.0.0-alpha.6 when a jest 30 prerelease was elsewhere in the graph, while jest itself stayed on 29.
With autoInstallPeers: false, a package's own optional peer dependencies are no longer added to its importer entry in pnpm-lock.yaml (and no longer linked into its node_modules) when another workspace project happens to resolve a matching version #13325.
overrides now also govern peers that pnpm auto-installs. Previously an override only rewrote dependencies declared in a manifest, so a peer nobody declares — installed because autoInstallPeers is on — resolved against its declared peer range and could bring in a second copy of the very package the override pinned. For example, with overrides: { react: npm:react@19.2.0 } and a lone lucide-react dependency, pnpm installed react@18.3.1; it now installs the pinned react@19.2.0 #13320.
pnpm approve-builds -g is accepted again, reporting that the command is not supported with global packages rather than failing with unexpected argument '-g' found. approve-builds was the only command that declared --global without its -g short form #13310.
The Rust implementation of pnpm has moved from alpha to beta releases.
A catalog name containing a control character no longer corrupts pnpm-workspace.yaml. pnpm add --save-catalog-name "$(printf 'a\nb')" (or the same value in saveCatalogName) now fails with ERR_PNPM_WORKSPACE_MANIFEST_WRITER_INVALID_CONTROL_CHARACTER and leaves the file untouched, matching how the writer already treats allowBuilds and overrides entries.
Preserve each direct dependency's locked optional peer context during pnpm dedupe.
Preserve optional peer providers recorded in peer suffixes when pnpm dedupe rebuilds a workspace lockfile.
With nodeLinker: hoisted, a workspace project no longer gets its own copy of a dependency whose version already won the workspace-root slot. Only the versions that lost the root slot are nested, matching the pnpm CLI. Previously every project's direct dependency was materialized under that project as well, which gave lifecycle scripts a second copy to run in.
The Rust engine now warns when package.json still declares install settings under the pnpm field, which pnpm 10 moved to pnpm-workspace.yaml. A project that hasn't migrated its pnpm.overrides / pnpm.packageExtensions / pnpm.patchedDependencies previously saw the settings silently ignored, and only met the downstream symptom. Keys the pnpm field never owned are left alone.
Fixed pnpm licenses list and pnpm licenses ls parsing and license metadata discovery when using the global virtual store pnpm/pnpm#13332 and pnpm/pnpm#13333.
A pnpm-workspace.yaml that declares a package pattern whose directory does not exist yet — packages/* before the first package is created, say — no longer fails every command with ERR_PNPM_WORKSPACE_WALK_ERROR. The pattern now matches no projects, as it does in the JavaScript implementation #13296.
Fixed catalog: references failing to resolve when installing through a pnpr server, which errored with "No catalog entry '<name>' was found for catalog 'default'." even though the catalog entry existed. The workspace the server reconstructs from the request has no catalog sections, so the client now sends its catalogs along with the request #13232.
Validate the project's pinned package manager and runtimes before running a command, matching the pnpm CLI:
packageManager / devEngines.packageManager pin that the running pnpm does not satisfy now fails with ERR_PNPM_BAD_PM_VERSION (or ERR_PNPM_OTHER_PM_EXPECTED when the project is pinned to another package manager), instead of being silently ignored. The check also runs under corepack, where pnpm cannot switch versions itself, and says so.devEngines.runtime / engines.runtime entries with onFail: "error" or onFail: "warn" are validated against the Node.js, Deno, or Bun installed on the system, failing with ERR_PNPM_BAD_RUNTIME_VERSION.pmOnFail and runtimeOnFail are honored as bypasses and can now be passed as --pm-on-fail=<value> / --runtime-on-fail=<value>, the form the error hints suggest.Global commands (--global) and commands that do not belong to the project (store, dlx, self-update, …) skip these checks, as does a project pin that only asked pnpm to switch versions when manage-package-manager-versions is turned off.
Arguments after the script or command name now reach the script untouched for pnpm run, pnpm exec, pnpm dlx, and pnpm with, matching the JavaScript implementation. Previously pnpm run build --config.foo=bar consumed the argument as a pnpm setting instead of forwarding it, and pnpm run build --silent handed the script --reporter=silent — a token the user never typed #13302. Put such flags before the script name (pnpm run --silent build) to apply them to pnpm.
Fixed generated lockfiles to preserve packages' scalar libc constraints.
Preserve a user-provided TMPDIR when scripts run with unsafePerm enabled; otherwise, continue using the package-local temporary directory.
Added support for publishConfig.name, which publishes a package under a different name than the one its manifest carries in the workspace. Only the published artifact is renamed — dependents, pnpm-lock.yaml, and release tooling keep addressing the project by its manifest name — and the new name reaches the packed manifest, the tarball filename, and everything that addresses the package at the registry: the already-published check of pnpm publish -r, its registry selection, and the release-planning probes of pnpm change status and pnpm version -r. This also fixes the changelog of the Rust CLI itself, which is published as pnpm from a workspace project named pacquet: its release notes were composed under the workspace name and so never made it into the published package #13345.
pnpm run <script> <args> now forwards every argument after the script name to the script verbatim, matching the behavior of the JavaScript implementation. Previously the -- separator was dropped, so pnpm run test -- --watch reached the underlying program as --watch and failed whenever that program claimed the option itself; arguments spelled like pnpm run's own flags (-s, --if-present) were also consumed by pnpm instead of reaching the script #13295. Pass those flags before the script name (pnpm run -s test) to apply them to pnpm.
pnpm test, pnpm start, and pnpm stop now forward their arguments to the script, matching the pnpm CLI. pnpm test --watch and pnpm start --port 3000 previously failed with a usage error, and pnpm stop claimed --if-present and -s for itself instead of passing them on. As with pnpm run, every token after the command name reaches the script verbatim, a -- separator included.
Added the --workspace-root (-w) flag, which runs the command on the root workspace project. pnpm add -D typescript prettier -w from a workspace subdirectory now saves to the root package.json instead of failing with "unexpected argument '-w' found" #13031. Combined with --recursive, the flag narrows the run to the root project alone. -w may not be used together with --global, and may only be used inside a workspace.
patchedDependencies patch files that pnpm applies no longer fail with ERR_PNPM_PATCH_FAILED: a hunk whose last line is context in a file with no final newline, and an LF patch against a CRLF file, both apply again #13322. A hunk that has drifted from its recorded line numbers is also retried nearby, matching pnpm.
A peer dependency is now recorded in the lockfile at the version and peer suffix the peer provider actually resolved to. Peers whose provider carried peer suffixes of its own could be recorded against a package instance that no importer installs, leaving an unreachable entry in snapshots: and a peer bound to the wrong instance #13320.
A peer dependency that the workspace root already provides is no longer installed a second time. With resolvePeersFromWorkspaceRoot enabled (the default), a missing peer is matched against the workspace root project's dependencies; it was matched against the dependencies of whichever project was being resolved, so a project that didn't declare the peer itself resolved its own copy from the registry. In vercel/next.js, whose overrides pin react to a single canary build, this pulled in a second react and paired it with react-dom from the canary — a combination the pin exists to prevent.
Under resolvePeersFromWorkspaceRoot, a workspace root dependency declared with link: or file: (or the path form of workspace:, such as workspace:../pkg) now satisfies another project's missing peer dependency at the linked package's own version, instead of being hoisted as a path. Those specifiers are relative to the project that declares them, so the same specifier reached a different directory — or none — from the project the peer was hoisted into, leaving a broken link. The root now has the same authority over the peer as it has when it declares the package with a version range #13373.
Two pnpm install peer-resolution fixes that made large workspaces such as Astro produce a different pnpm-lock.yaml than pnpm 11 #13334:
dependencies and peerDependencies no longer gets a nested copy of it when the parent already supplies that name, which is what pnpm does with autoInstallPeers disabled. The nested copy hid the peer, so the package was recorded without the peer context it resolves in.Closed the remaining gaps in how unscoped per-registry .npmrc settings are pinned to the registry their own source file declared:
cert= / key= written with \n escapes now expands to a real multi-line PEM, matching the URL-scoped //host/:cert= spelling.pnpm config get / pnpm config list now report a rescoped credential under the URL-scoped key it was pinned to, instead of the unscoped key it was written as.tokenHelper.@pnpm/napi bindings: the authHeaderByUri entry written with an empty ("") key is pinned to the registry / registries.default the host passed alongside it, never to a registry the project's .npmrc names.The projects that run their own lifecycle scripts (preinstall, install, postinstall, prepare, …) now match pnpm in every install-family command. A project runs them when the command installs it in full, and — in a workspace the command only partly covers — whenever the command mutates it at all; the workspace root runs them even when the command was pointed at another project, because it is installed in full alongside it. As a result, pnpm update <pkg> and pnpm add <pkg> in a workspace no longer skip the workspace root's scripts, pnpm update at a workspace root no longer runs the other members' scripts, and pnpm update --latest no longer runs the project's own scripts (it rewrites named dependency specs, so it is a partial install like pnpm update <pkg>) #13358.
Print the script command by default when running a filtered lifecycle script. The command remains hidden with --silent.
Run dependency verification consistently after regenerating a lockfile with dedupePeers enabled.
Aligned the hoistedDependencies contents and ordering in node_modules/.modules.yaml with pnpm.
Preserve whether package libc metadata uses a string or an array when writing the lockfile.
A package.json that starts with a UTF-8 byte order mark is read again instead of failing with expected value at line 1 column 1. Workspace discovery, dependency manifests (including bin linking), tarball extraction, and pnpm publish of a pre-built tarball all accept one, matching pnpm #13311. A manifest that really is malformed now reports its path in the error.
Fixed ERR_PNPM_BROKEN_LOCKFILE when installing with a pnpm 10 lockfile that has a patchedDependencies section. See pnpm/pnpm#13307.
pnpm -r run "/pattern/" --no-bail no longer exits zero when one of a project's matched scripts fails and a later one passes. The run summary carries a single status per project, and the passing script overwrote the recorded failure.
Resolution failures now report the error pnpm defines for them. A well-formed range that the registry publishes nothing for fails with ERR_PNPM_NO_MATCHING_VERSION — naming the latest release, the other dist-tags, and the pnpm view <pkg> versions command that lists the rest — instead of ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER. A package the registry doesn't have fails with ERR_PNPM_FETCH_404 and the "not in the npm registry, or you have no permission to fetch it" hint (plus which authorization header was sent, since a private registry often answers a permission failure with a 404) instead of a bare HTTP-client message. A wrapper that quotes its cause verbatim no longer prints the same sentence twice in the error report.
Fixed pnpm add <workspace-package> to resolve the local package when linkWorkspacePackages is enabled.
$dep-name self-references in overrides are now resolved against the root manifest's direct dependencies, so an override such as rolldown: $rolldown records the concrete specifier in pnpm-lock.yaml and no longer fails a frozen install with ERR_PNPM_OUTDATED_LOCKFILE #13314. A reference to a package that is not a direct dependency fails with ERR_PNPM_CANNOT_RESOLVE_OVERRIDE_VERSION, and the deprecated syntax now warns, pointing at catalogs.
The root project's pnpm:devPreinstall script now runs before resolution and linking, as it does in pnpm 11. It is skipped under --ignore-scripts, --lockfile-only and --dry-run, by pnpm fetch and pnpm rebuild, and by a repeat install that is already up to date. Workspaces that use the hook to prepare state the install depends on — such as next.js, which generates a placeholder next bin with it — were left with dependents linked against files that were never created #13313.
The lockfile-verification line now dates a cached verdict — ✓ Lockfile passes supply-chain policies (verified 253ms ago) — instead of the timeless (previously verified) #13315.
pnpm install, run, test, update, remove, link, unlink, prune, and rebuild now print the workspace scope they resolved — Scope: all 41 workspace projects, or Scope: 5 of 41 workspace projects under a --filter. This is the confirmation that a filter selected what was intended #13315.
An install that blocks a dependency's build scripts now appends a placeholder for it to pnpm-workspace.yaml, so approving or denying the build is an edit rather than writing the block by hand:
allowBuilds:
es5-ext: set this to true or false
A placeholder is not a decision — the build stays blocked until it is replaced with true or false — and an existing entry is never overwritten #13315.
Fixed --parallel being treated as the script name when placed before run in a recursive command.
scriptShell now selects the shell for lifecycle scripts too — dependency build scripts and a project's own preinstall/install/postinstall/prepare and pnpm:devPreinstall — not only for pnpm run and pnpm exec. A workspace that configures a shell was still getting the platform default (sh / cmd) for everything the install itself spawns.
Fixed shamefullyHoist: true to create public root dependency links.
Prevented optional peers from being selected from an unrelated workspace package's shared dependency context.
The store index now keys URL, git-host, and type: git dependencies by their bare resolution id, matching the key pnpm 11 writes #13365. Previously these rows carried a <name>@ prefix, so a store warmed by one pnpm major was cold for the other and every non-registry dependency was re-downloaded, re-extracted, and re-imported on a switch. A remote tarball also occupied two index rows instead of one, doubling its extraction work.
A project that pins pnpm through devEngines.packageManager (or a v12+ packageManager field) now gets its packageManagerDependencies recorded in pnpm-lock.yaml by every command, not just by the install-family ones #13348. Running pnpm list (or any other command) in a freshly cloned project no longer leaves the lockfile without the pinned version. The pmOnFail setting now also decides whether the pin is recorded: --pm-on-fail=ignore keeps it out of the lockfile even when the manifest asks for a stricter policy, and vice versa.
Fixed pnpm licenses list to detect licenses from license files and preserve the latest package version's development classification.
pnpm update --latest now rewrites jsr: dependencies. The manifest keeps the protocol and the range operator it declared, so jsr:1.0.0 becomes jsr:2.0.0 and jsr:@scope/name@^1.0.0 becomes jsr:@scope/name@^2.0.0, instead of being left at the old version #13363.
pnpm update --latest now rewrites dependencies using a named registry alias. The manifest keeps the alias prefix and the range operator it declared, so gh:1.0.0 becomes gh:2.0.0 and gh:@acme/foo@^1.0.0 becomes gh:@acme/foo@^2.0.0, instead of being left at the old version pnpm/pnpm#13393.
Breaking change from pnpm v11. Under engineStrict, an install 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 v11 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.
A lockfile entry whose tarball resolution records no integrity is now reported by the lockfile-verification gate, before anything is downloaded: every offending entry is listed in one ERR_PNPM_MISSING_TARBALL_INTEGRITY error instead of failing the install one fetch at a time after the gate had already passed the lockfile #13364. An integrity: '' that pins nothing is treated the same as a missing one, and the exemption for git-host archive URLs is now read from the URL rather than the lockfile's own gitHosted marker.
Fixed pnpm licenses list to read licenses from legacy package manifest fields.
Fixed --workspace-root (-w) selecting the current workspace when --dir pointed at a nonexistent directory outside it (for example pnpm --dir ../../elsewhere add -w foo). The command now fails with ERR_PNPM_NOT_IN_WORKSPACE, matching pnpm. A nonexistent --dir inside the workspace still resolves to the workspace root as before.
One column per quarter.
pnpm setup now appends PNPM_HOME and the global bin directory to the GitHub Actions environment files (GITHUB_ENV and GITHUB_PATH), so later steps in
pnpm setup now appends PNPM_HOME and the global bin directory to the GitHub Actions environment files (GITHUB_ENV and GITHUB_PATH), so later steps in the same job can run pnpm add --global and other global commands #9191.Checking GitHub Actions dependencies for updates is now opt-in for every command. Neither pnpm outdated nor pnpm update reads the workflow files unless --include-github-actions is passed or update.githubActions is set to true in pnpm-workspace.yaml. Reading them runs git ls-remote against every referenced repository, which fails in environments where GitHub is not reachable the way pnpm assumes (a GitHub Enterprise Server, a custom certificate authority, or an offline network) #13254.
pnpm outdated accepts the --include-github-actions option too.
pnpm update --latest now keeps dependencies that the npm registry does not serve in the form they were declared. A runtime: dependency (such as "node": "runtime:26.5.0"), a git/github: URL, or a remote tarball URL previously had its name looked up on the npm registry and its specifier overwritten with that unrelated package's version.
pnpm update --latest also no longer rewrites package.json when a dependency is already at its latest version.
Close three CLI parity gaps with the TypeScript pnpm CLI:
Close three CLI parity gaps with the TypeScript pnpm CLI:
--registry <url> is now accepted on every command as a universal rc-option, not only through --config.registry=<url> (pnpm view pnpm dist-tags.latest --registry=https://registry.npmjs.org/).pnpm add (and pnpm add -g) now accept --allow-build=<pkg>, appending the named packages to allowBuilds so they can run their lifecycle scripts during the install (pnpm add @pnpm/exe@11.16.0 --allow-build=@pnpm/exe).--dir / -C is now position-independent: it is accepted anywhere on the command line, before or after the subcommand (pnpm add foo --dir /tmp/proj).pnpm publish --provenance now applies the fetch-timeout setting to the sigstore signing exchange and retries it up to two more times with exponential backoff when it fails or times out, instead of aborting the publish on the first transient network error or hanging on a stalled connection.
pnpm update --latest now resolves a dependency declared through an npm: alias — directly in package.json or in the catalog entry a catalog: reference points to — to the latest version of the aliased package, keeping the npm:<name>@ prefix in the rewritten specifier. Previously the alias name itself was looked up on the registry, failing the update with a 404 when no package of that name exists.
Nothing published for this version
The deprecated updateConfig, auditConfig, and auditLevel settings keep working until the next major version. When both a new section value and its dep…
The first release of a package now publishes the version written in its manifest verbatim, instead of bumping off it. pnpm version -r and pnpm change status check the registry for each release's current version; when that version is not yet published, the package debuts at it and its pending changesets apply only from the next release. A newly added package seeded at 1100.0.0 with a minor changeset is therefore published as 1100.0.0 rather than skipping straight to 1100.1.0.
pnpm version now supports the npm-style bump forms: pnpm version <major|minor|patch|premajor|preminor|prepatch|prerelease> and pnpm version <exact-version> (also recursively with -r), with --preid, --allow-same-version, --message, --no-git-tag-version, --no-commit-hooks, --sign-git-tag, --tag-version-prefix, and --json. The bump runs the preversion/version/postversion lifecycle scripts and records the new version as a git commit and tag.
Added recursive workspace support to pnpm outdated. pnpm list and pnpm ll now inspect all workspace projects by default, matching the TypeScript CLI.
Made recursive pnpm rebuild honor workspace filters with shared and dedicated lockfiles.
Made pnpm why and pnpm peers recursive by default in workspaces. Recursive peer checks now honor workspace filters, and recursive why can inspect the active project when a workspace uses dedicated lockfiles.
Added a --changeset flag to pnpm update. Set update.changeset to true in pnpm-workspace.yaml to enable this behavior by default, and use --no-changeset to override the setting for one update. After the update completes, pnpm writes a .changeset/pnpm-update-<suffix>.md file declaring a patch bump for every workspace package whose dependencies or optionalDependencies were changed by the update and a major bump when peerDependencies changed, including packages that consume an updated catalog entry via the catalog: protocol. Private packages, packages without a name, and packages listed in the ignore array of .changeset/config.json are skipped. If .changeset/config.json does not exist, a warning is printed and no changeset is generated.
Added GitHub Actions dependencies to pnpm outdated and interactive pnpm update. Non-interactive updates can include them with --include-github-actions or by setting update.githubActions to true in pnpm-workspace.yaml. Updated actions are pinned to exact commit hashes with their release tags preserved in comments.
Fixed pnpm install rewriting unrelated pnpm-lock.yaml entries after a small manifest change — for example, removing one dev dependency could bump other packages' open-range dependencies (such as jest's @types/node: '*') to their newest versions pnpm/pnpm#13193. Three resolution-reuse gaps caused still-satisfied lockfile entries to be re-resolved from the registry:
catalog: protocol were compared against the lockfile in their resolved-range form, so every catalog-managed dependency looked changed on every install, and any package depending on one was re-resolved.pnpm install, pnpm add, pnpm update, and pnpm remove now support recursive (-r) and filtered (--filter) execution in workspaces configured with one lockfile per project (sharedWorkspaceLockfile: false), instead of failing with ERR_PNPM_RECURSIVE_SHARED_LOCKFILE_UNSUPPORTED. Each selected project is installed independently against its own pnpm-lock.yaml, node_modules, and virtual store, matching pnpm.
Global commands (pnpm add -g, pnpm runtime set -g, ...) now create a missing global bin directory instead of failing with ERR_PNPM_PNPM_DIR_NOT_WRITABLE, and the universal --silent / -s shorthands for --reporter=silent (e.g. pnpm store path --silent) are supported again.
pnpm unlink now reinstalls through the selection-aware install pipeline, matching pnpm: it honors -r / --filter, installs recursively by default inside a workspace, and supports both a shared workspace lockfile and one lockfile per project (sharedWorkspaceLockfile: false). Previously it always reinstalled only the active project.
Added update and audit settings sections to pnpm-workspace.yaml, superseding the awkwardly named updateConfig, auditConfig, and top-level auditLevel settings:
update:
ignoreDeps: # was updateConfig.ignoreDependencies
- webpack
- "@babel/*"
audit:
level: high # was auditLevel
ignore: # was auditConfig.ignoreGhsas
- GHSA-xxxx-yyyy-zzzz
update.ignoreDeps lists dependency name patterns that pnpm update and pnpm outdated should skip. audit.level and audit.ignore tune pnpm audit.
The deprecated updateConfig, auditConfig, and auditLevel settings keep working until the next major version. When both a new section value and its deprecated counterpart are set, the new section takes precedence and a warning is printed. Both the TypeScript CLI and the Rust config surface (pacquet) recognize the new sections.
Added support for alias-less Git dependency adds, preserved locked Git commits during unrelated dependency changes, and reported Git package versions
Added support for alias-less Git dependency adds, preserved locked Git commits during unrelated dependency changes, and reported Git package versions in install logs.
pnpm list and pnpm why are now feature complete and behaviorally identical to the TypeScript CLI. pnpm list gained --only-projects, --find-by (finders declared in .pnpmfile.cjs), search by version range (pnpm ls "foo@^2"), subtree deduplication with [deduped] markers, peer/skipped annotations, the package-count summary, --long manifest details, resolved tarball URLs and absolute paths in --json/--parseable output, and --depth support for globally installed packages. pnpm why gained --json, --parseable, --long, --prod/--dev/--no-optional, --find-by, workspace project names in the reverse tree, dependency-field annotations, [circular]/[deduped] markers, peer-variant hashes, and the Found N versions summary.
Added support for the cleanupUnusedCatalogs setting: when enabled, pnpm add, pnpm update, and pnpm remove drop catalog entries from pnpm-workspace.yaml that no workspace project references.
The enableModulesDir: false setting is now honored: the install resolves and writes pnpm-lock.yaml but creates no node_modules directory (unless the global virtual store is enabled, in which case packages are still materialized into the store).
Command shims now set NODE_PATH the way pnpm does: under the isolated nodeLinker with a hoist pattern, each shim lists the target package's own node_modules directories followed by the hidden hoisted modules directory (node_modules/.pnpm/node_modules). The new extendNodePath: false setting turns this off.
Added the --force flag to pnpm install and pnpm add: optional dependencies whose cpu / os / libc / engines don't match the host are installed instead of skipped, and a forced install relinks packages that an earlier install already materialized #13142.
sharedWorkspaceLockfile: false is now supported by the install family #12042: a workspace install runs one dedicated install per project, each with its own pnpm-lock.yaml, node_modules, and virtual store (a custom virtualStoreDir resolves per project), and pnpm add / update / remove in a project operate on that project's own lockfile. Recursive and filtered install-family commands still require a shared lockfile.
Added PnP install materialization and fixed recovery from expired module caches and broken private lockfiles.
Fixed recovery from interrupted dependency builds in the global virtual store, and made pnpm fetch populate the virtual store without linking dependencies into projects.
Fixed workspace lifecycle ordering and bin linking across isolated and hoisted installs.
Auto-installed peer dependencies wanted by multiple packages under distinct but compatible ranges now resolve through the ranges' semver intersection (2 + ^2.2.0 install one provider matching >=2.2.0 <3.0.0-0), matching pnpm. Previously such peers were only auto-installed when every consumer declared the identical range or autoInstallPeersFromHighestMatch was enabled.
engineStrict now fails the install when an incompatible package is reached through a regular dependency edge of an installable package, even if the package is also optionally reachable — matching pnpm. Packages reachable only through optional edges or skipped parents are still skipped #13143.
Engine checks (engines.node / engines.pnpm) now match npm-semver's includePrerelease semantics exactly: a prerelease version no longer satisfies a fully specified >= bound (9.0.0-alpha.1 does not satisfy >=9.0.0), while still satisfying expanded ranges like 9, >=9, and ^9.0.0.
Fixed a rare hang where pnpm install or pnpm add could wait forever: when two tasks fetched the same tarball concurrently, the waiting task could miss the downloader's completion notification and never wake up.
Deprecated packages are reported during installation: a directly depended-on deprecated package gets an immediate warning, and deprecated subdependenc…
Completed pnpm runtime installation parity for Node.js, Deno, and Bun, including runtime failure policy, target architecture selection, and dependency runtime engines. Runtime failure overrides now preserve explicit runtime dependencies without matching engine entries.
Deprecated packages are reported during installation: a directly depended-on deprecated package gets an immediate warning, and deprecated subdependencies are summarized in a single <N> deprecated subdependencies found line. Versions matched by pnpm.allowedDeprecatedVersions are not warned about #11633.
Implemented native install-test command.
Implemented native recursive, multi, and m commands in the Rust CLI.
Added the virtualStoreOnly setting, which populates the virtual store without any post-import linking — no importer symlinks, no .bin entries, no hoisting, and no project lifecycle scripts. Combining it with enableModulesDir: false fails with ERR_PNPM_CONFIG_CONFLICT_VIRTUAL_STORE_ONLY_WITH_NO_MODULES_DIR unless enableGlobalVirtualStore is on, since the standard virtual store lives inside node_modules. A subsequent ordinary install completes the linking instead of treating the partially-populated directory as up-to-date. enableModulesDir is now read from pnpm-workspace.yaml as well.
Repeat installs now reconcile the existing node_modules the way the TypeScript CLI does: direct dependencies removed from the lockfile lose their links and bin shims, hoisted aliases of removed packages are unlinked so the next hoist pass can claim their slots, a hand-deleted package is detected and re-installed even when the lockfile is otherwise up to date, and pnpm add / pnpm remove fail with ERR_PNPM_HOIST_PATTERN_DIFF-family errors instead of silently recreating a modules directory whose layout settings drifted. Dev-only installs also no longer delete node_modules/.pnpm/lock.yaml.
pnpm install --ignore-scripts now records the builds it skipped in node_modules/.modules.yaml's pendingBuilds, and pnpm rebuild --pending runs them and clears the record instead of finding nothing to do. Both the dependencies whose build scripts were suppressed and the workspace projects whose own install scripts were suppressed are recorded and re-run, and an install that removes a package drops it from the list.
pnpm install now fails with ERR_PNPM_UNUSED_PATCH when an entry in patchedDependencies doesn't match any installed package. Set allowUnusedPatches: true in pnpm-workspace.yaml to get a warning instead, matching pnpm 11 #11633.
pnpm add no longer drops the other dependency groups from the install: adding a package with optionalDependencies no longer leaves dangling optional-dependency symlinks in the virtual store (pnpm add -g @openai/codex produced a codex bin that failed with "Missing optional dependency @openai/codex-darwin-arm64"), and a production pnpm add no longer removes the project's devDependencies from pnpm-lock.yaml and node_modules.
pnpm add with --save-dev, --save-optional, or --save-prod now moves an already-declared dependency to the target group instead of leaving a duplicate entry in its old group, matching pnpm.
pnpm add <pkg> without a --save-* flag now updates an already-declared dependency in the group it occupies (devDependencies / optionalDependencies), matching pnpm, instead of always saving it into dependencies.
pnpm install now detects a supportedArchitectures change and re-evaluates previously skipped platform-specific optional dependencies, instead of reporting the project as up to date and leaving the packages for the old architecture set in place.
Avoided optimistic repeat-install shortcuts when a lockfile contains merge conflict markers.
pnpm setup now removes leftover v10-layout shims at the top of PNPM_HOME, so pnpm self-update no longer warns about a v10 installation layout after PATH has been migrated to the v11 PNPM_HOME/bin layout. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, self-update also no longer treats a dangling legacy shim (one whose install target was garbage-collected) as a real v10 layout, so the warning can no longer fire on dead shim files.
Closes pnpm/pnpm#12496.
A git-hosted dependency with no host archive (an ssh, self-hosted, or git+file: repo) whose package name matches the dependency's alias now records the bare git+<repo>#<commit> reference in the lockfile's importer entry, matching pnpm's pnpm-lock.yaml output instead of prefixing it with <name>@.
A private git-hosted dependency resolved over HTTPS with an embedded auth token (git+https://<token>@github.com/owner/repo.git) is now recorded as a type: git resolution against the authenticated remote, instead of being rewritten to the host's public archive URL (a codeload.github.com tarball) that carries none of those credentials and so could not be fetched.
When a dependency's build script fails under enableGlobalVirtualStore, the global virtual store directory it was being built in is now removed for scoped packages too. Previously the cleanup resolved one directory level short of the hash directory for a scoped name, leaving a half-built directory behind that later installs would reuse.
A hoisted-linker install no longer fails with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY when an optional dependency's snapshot is absent because it was skipped on a previous install.
Fixed patched dependencies being applied to only one copy of a package under nodeLinker: hoisted. When a version conflict kept a patched package out of the root node_modules, the hoisted layout nested a copy of it under each consumer that needed it, but only the first copy was patched — every other copy silently ran the unpatched code the patch existed to replace. The same gap applied to a reinstall served from the side-effects cache. Every copy is now patched, matching nodeLinker: isolated and pnpm's behavior.
pnpm install now announces Lockfile is up to date, resolution step is skipped whenever the headless installer runs — including installs that materialize a cold node_modules from an up-to-date lockfile and --filter subset installs — matching the TypeScript CLI. pnpm fetch prints Importing packages to virtual store on that path instead.
Conditional metadata requests send If-Modified-Since as an HTTP-date instead of the mirror's raw ISO-8601 modified value, so registries can answer 304 Not Modified instead of re-serving the full packument #13104.
Fixed two global-virtual-store correctness gaps. A failed build now discards the hash directory it was building in, so the next install re-fetches instead of reusing a half-built directory shared by every project with the same dependency graph. The removal only ever touches a slot strictly inside the store, so a crafted package name cannot make it escape. And a side-effects-cache hit no longer assumes the store slot still holds the cached build: when the slot has been re-imported pristine, the build output is materialized rather than skipped, which previously left the package without its build artifacts.
.modules.yaml now records the allowBuilds set the install ran under, matching pnpm.
Aligned large-download progress byte formatting with pnpm.
Changing --os / --cpu / --libc or supportedArchitectures between installs now re-evaluates previously skipped optional dependencies, so the platform packages for the newly selected architecture are installed instead of staying skipped.
Removing a package from allowBuilds now fails the next pnpm install under strictDepBuilds instead of reporting the project as already up to date. A build whose output is already cached in the store no longer counts as an approval #11035.
.modules.yaml now records the dependencies of a skipped optional package in skipped as well, matching pnpm: when a platform-incompatible optional package is skipped, its own dependency subtree is not materialized either.
Fixed proxy settings from the global config.yaml and command-line options in pnpm.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
pnpm audit --fix now updates vulnerable packages in a single project that sets updateConfig.ignoreDependencies . It used to leave them on the vulnerab…
pnpm 11.28.3 updates undici to clear a security advisory, fixes "database disk image is malformed" errors when several pnpm processes share a store, and makes packages, catalogs, projects, and commands named like constructor work.
pnpm now ships undici 7.29.1, so security scans of pnpm no longer report GHSA-3wwx-pv8p-q78v.
pnpm no longer fails with "database disk image is malformed" or reads stale store entries while another pnpm process writes to the same store.
Names that match built-in JavaScript object properties, such as constructor, toString, or __proto__, now work like any other name. pnpm crashed, wrote a wrong lockfile, or silently skipped such names in:
pnpm add, pnpm install, and pnpm import, for dependencies, peer dependencies, and file: dependencies that point to a directory named constructor.toString was not written.$toString.pnpm list, and pnpm why.pnpm constructor runs the constructor script like any other unknown command, and pnpm help constructor no longer crashes.constructor.pnpm install no longer re-resolves an up-to-date lockfile on every run when a patched package is a peer in a peer cycle #16418.
POSIX bin shims and the pnpm, pn, pnpx, and pnx launchers now run inside a Nix build, where the system default path holds none of the utilities they call. Installing again replaces the shims already in node_modules #16377.
In a project that pins another pnpm version, pnpm now passes a command with an option it does not know to the pinned version. Before, pnpm install --auto-dedupe failed with "Unknown option" even though the pinned pnpm supports it #16353.
pnpm now fails with ERR_PNPM_INVALID_ALLOW_BUILDS when allowBuilds is not an object or one of its values is not true, false, or a string. Such values used to be ignored silently.
Removing a dependency whose bins are declared through directories.bin no longer leaves broken shims in node_modules/.bin.
A custom resolver's shouldRefreshResolution hook that rejects no longer crashes pnpm with an unhandled rejection when another hook has already asked for a refresh.
When minimumReleaseAge hides the version that latest points to, pnpm now falls back to a prerelease of the same major before a stable version of an older major. A stable version of the same major is still preferred. For example, while a new 1.0.0 is too new, pnpm picks 1.0.0-beta.4 rather than an old 0.0.1 #16388.
pnpm --filter <project> update <pkg> now fails with ERR_PNPM_NO_PACKAGE_IN_DEPENDENCIES when the selected projects do not depend on <pkg>, also in a workspace with a shared lockfile and a root project. It used to exit successfully.
pnpm audit --fix now updates vulnerable packages in a single project that sets updateConfig.ignoreDependencies. It used to leave them on the vulnerable version.
pnpm update --global now removes hard-linked executables from PNPM_HOME when migrating packages from the old global layout #16420.
Updating a pinned GitHub Action now rewrites the version in its # vX.Y.Z comment even when the action name contains the same version text. The action name used to change while the comment kept the old version.
pnpm deploy no longer fails with ERR_PNPM_DEPLOY_AMBIGUOUS_PEER in a workspace with injectWorkspacePackages: true when a workspace package also lists its peer dependency as a dev dependency #16375.
pnpm deploy no longer copies the workspace root's packageManager and devEngines.packageManager fields into the deployed package.json #16403.
--filter fixes:
...pkg... selector combined with another dependents selector, such as --filter ...a --filter ...b..., no longer adds the dependencies of the other selector's dependents.--filter "[<since>]" now detects changes in projects whose directory names contain non-ASCII characters. The change used to be credited to the parent project.After relaying a signal to a script, pnpm keeps waiting for a process in the script's process group whose main thread has exited while its other threads still run. Linux reports such a process as a zombie, so the wait used to end before those threads finished pnpm/tasks#56.
A lifecycle script run with unsafePerm: false now fails with an error when pnpm cannot create node_modules/.tmp. It used to hang.
pnpm run with verifyDepsBeforeRun no longer crashes with an unhandled rejection when a lockfile it did not need to compare fails to load.
pnpm run -r now closes the collapsible CI log section of a project whose script fails, so the output of later projects is no longer nested inside it.
pnpm run --resume-from no longer crashes when a saved run state file contains null.
pnpm store prune now removes the packages that only expired pnpm dlx cache entries used, as long as the store still has another registered project. They used to stay until the next pnpm store prune #16383.
pnpm store prune now stops with an error when it cannot read a project directory for a reason other than the directory missing, such as a permission error. It used to skip the directory.
pnpm publish now includes bare README files and README files with Markdown extensions such as readme.markdown in registry metadata #12704.
pnpm pack-app now accepts an entry file or output directory inside the project whose name starts with two dots, such as ..build/entry.cjs. It used to fail with ERR_PNPM_PACK_APP_ENTRY_OUTSIDE_PROJECT.
Registry error messages now always say "(response body truncated)" when pnpm cut the response body short. The marker was missing when the body was cut at exactly 64 KiB.
pnpm 11.28.2 fixes pnpm install skipping every workspace project whose common ancestor is the filesystem root, and stops pnpm run from reinstalling or
pnpm 11.28.2 fixes pnpm install skipping every workspace project whose common ancestor is the filesystem root, and stops pnpm run from reinstalling or installing when nothing needs it.
pnpm install reported success without installing anything when the workspace projects' common ancestor was the filesystem root, such as / or a drive root like C:\. It now installs these projects #16328.
verifyDepsBeforeRun no longer reports dependencies as outdated after a filtered install just because pnpm-lock.yaml has a newer modification time. It checks the lockfile against the packages that install put in place. Before, pnpm run reinstalled the whole workspace with lifecycle scripts on, for example after a Docker COPY brought in a lockfile with a newer mtime #16322.
After a filtered install, verifyDepsBeforeRun now also checks that the install put the selected projects' dependencies in place. A node_modules directory alone no longer counts as proof.
pnpm run and pnpm exec no longer install a project that has never been installed and has nothing to install. Such a project declares no dependencies, no peer dependencies that autoInstallPeers would fetch, and no install lifecycle scripts. The command now runs without writing node_modules or pnpm-lock.yaml #16313.
pnpm 11.28.1 makes pnpm install work in StackBlitz WebContainers, repairs lockfiles whose patch hashes disagree with patchedDependencies , and fixes m
pnpm 11.28.1 makes pnpm install work in StackBlitz WebContainers, repairs lockfiles whose patch hashes disagree with patchedDependencies, and fixes many bugs in Windows command shims, injected workspace dependencies, and pnpm deploy.
pnpm install now works in StackBlitz WebContainers. On projects without a lockfile, it used to fail with ENOENT ... pnpm-lock.yaml, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls. It also failed with "this.db.exec is not a function", because node:sqlite there lacks DatabaseSync.exec. When node:sqlite cannot prepare statements either, pnpm stores the index in index.fallback #15649.
pnpm install now completes after downloading a Node.js runtime specified by devEngines.runtime when pnpm runs on Node.js 24.4.x #14667.
pnpm install no longer fails when a package from the registry declares a file: dependency on a directory inside itself, such as "@types/css-tree": "file:./typings/css-tree". pnpm links that dependency to the directory inside the package, as npm and Yarn do. The lockfile records it as link:<root>/typings/css-tree #9141.
Installing a git-hosted dependency that has to be built no longer fails when that dependency's own dependencies have build scripts nobody approved. pnpm skips those builds while preparing the dependency, as it does without strictDepBuilds #9764.
pnpm now uses pnpm to prepare a git-hosted dependency that is a pnpm workspace without a committed lockfile. It used npm before, which could skip the dependency's build #14011.
When installing a git dependency over SSH fails with Permission denied (publickey), pnpm suggests checking the loaded keys with ssh-add -l. Resolving an SSH URL that refuses the key also shows a local HTTPS rewrite that leaves the recorded URL alone #13743.
pnpm install --dev and pnpm fetch --dev now install the optional dependencies of devDependencies, such as the platform binaries of Biome and oxlint. The project's own optionalDependencies are still skipped #9678.
pnpm install --frozen-lockfile now works on a detached HEAD when gitBranchLockfile is enabled. The install now reads the lockfiles of the local and remote-tracking branches that contain the checked-out commit. It still writes the shared pnpm-lock.yaml #7672.
pnpm install on CI now fails on an outdated lockfile when preferFrozenLockfile is explicitly set to true. Setting it to true used to let CI update the lockfile #9072.
pnpm install now fails with ERR_PNPM_IGNORED_BUILDS on a repeat install when strictDepBuilds is on and a dependency's build is still undecided. A repeat install against an existing node_modules reported success where a fresh install failed #10450.
pnpm install now removes an optional dependency from node_modules if its install script fails. Code that checks whether the package is installed no longer finds a package that cannot load #8756.
pnpm install --offline and pnpm add --offline now resolve a version range to the newest matching version whose tarball is already in the store. They used to pick the newest version in the cached metadata and fail with ERR_PNPM_NO_OFFLINE_TARBALL when its tarball was missing #10715.
If an offline install fails because the registry metadata cache uses the layout from before pnpm 11.27 and 12.4, the error now names the older mirror on disk and explains that one online install repopulates the cache #15656.
pnpm install now fails right away when writing package files fails because the store is full. It no longer retries the tarball download first #8581.
With nodeLinker: hoisted, pnpm install now restores a workspace project's node_modules after it was deleted. Before, the install printed "Already up to date" and left the project without the dependencies nested under it.
Under nodeLinker: hoisted, pnpm install now clears orphaned package directories that an interrupted or failed install leaves in a project's node_modules. A directory recorded by the previous install is removed, while an unrecorded directory is moved to node_modules/.ignored. A copy already in .ignored is never overwritten #13676.
Packages in an external virtualStoreDir can resolve the project's direct dependencies selected by hoistPattern. Run pnpm install --force to repair an existing installation #5652.
A repeat install now keeps the fast path when a declared local file dependency is replaced by an override #12892.
Files imported from the store now follow the umask of the install that writes them. Installing with a umask of 077 no longer leaves imported files readable by the group and others #3807.
With the global virtual store, pnpm rebuild no longer modifies packages shared with projects that have not approved their build scripts #12302.
The side-effects cache now restores the symlinks that a build script creates inside a package. A warm install used to replace each of them with a copy of its target #12859.
After upgrading, every package with a build script is built once more.
pnpm install now restores cached build artifacts when reinstalling a workspace that uses separate lockfiles #12942.
The global virtual store and the side-effects cache now key built packages by the Node.js version that the root project's devEngines.runtime or engines.runtime pins. That is the Node.js their build scripts run with. A dependency that declares its own engines.runtime no longer changes the key for every other package.
Concurrent installs that share a global virtual store now run a package's build in its shared slot one at a time. A failed build leaves the slot in place and marks it for the next install to rebuild #15568.
Concurrent installs no longer fail when they replace the same stale hoisted dependency link. Virtual store cleanup now keeps the temporary lockfiles that concurrent installs are still writing.
pnpm install keeps the owner, group, and mode of files already in a shared store, including index.db. New store files and directories inherit the store directory's group-write bit. When that directory is setgid, new files inherit its group. pnpm does not change a file's owner or group #12765.
When pnpm install repairs a store file that was modified through a hard link in node_modules, the repair now keeps the file's inode on Linux and macOS. Hard-linked copies in other projects are healed at the same time. Before, only the project running the install received the restored content. On Windows the repair still replaces the file, so other projects are healed on their next install #3445.
A tarball whose integrity pnpm computed during download is now found in the store on the next install. Before, that install downloaded the tarball again once the lockfile recorded the integrity #12562.
pnpm now warns when it cannot hard link packages from an existing store in the pnpm home directory and uses a store on the project's filesystem. This can happen when the project is on another filesystem, such as a bind-mounted workspace in a container. The warning names both stores and suggests setting storeDir #14505.
A peer dependency no longer resolves to two different versions for one package. This happened when the package peer-depends on another package and on one of that package's peers, and it is installed deeper than a direct dependency of the package that provides them #12098.
An optional peer dependency is no longer resolved from another workspace project's package when the project provides one of that package's own peers at a version it rejects. This avoids bogus unmet peer errors #13989.
pnpm no longer reports unmet peer dependency warnings for aliased npm: peer ranges that a tarball dependency satisfies #11126.
pnpm install now links the executables of auto-installed peer dependencies into the workspace root's node_modules/.bin, including after a frozen-lockfile reinstall #8511.
With resolutionMode: time-based and minimumReleaseAge both set, pnpm install no longer reports a subdependency as too new when only the time-based cutoff excludes it. Such subdependencies used to fail a strict install with ERR_PNPM_NO_MATURE_MATCHING_VERSION, or were added to minimumReleaseAgeExclude #13569.
A transitive dependency that has no matching version published before the time-based cutoff now resolves to the lowest matching version allowed by minimumReleaseAge. pnpm picks a version younger than minimumReleaseAge only if no older version matches #16298.
With minimumReleaseAge set, re-resolving the lockfile no longer rewrites the peerDependencies recorded for a package whose version did not change. This happened when the registry metadata of a package differed from the package.json in its tarball #13988.
pnpm no longer revalidates cached registry metadata when the registry sends Cache-Control: max-age=0, no-cache, or no-store. It downloads the metadata again, so a version newly published to such a registry is visible on the next install #13487.
pnpm's built-in package compatibility database no longer applies to a project's own manifest. A project named like a published package, such as vue-loader, no longer gains dependencies on pnpm install or pnpm update. User-configured packageExtensions still apply to project manifests #11700.
Relative local tarball paths in pnpm.overrides without an explicit file: prefix are now rebased correctly for workspace packages #11131.
pnpm no longer reports pnpm-lock.yaml as broken when a project depends on a package named constructor. A __proto__ key in the lockfile is now kept as a plain entry when pnpm reads or writes the lockfile. It no longer replaces the prototype of the objects pnpm builds from it #11028.
With nodeLinker: pnp, a workspace package can now require another workspace package it depends on #3567. On Windows, workspace dependency paths in the generated .pnp.cjs now use forward slashes.
pnpm now uses less memory when installing a package whose archive is larger than 64 MiB unpacked, and when installing a runtime from a zip archive, such as Node.js on Windows, Deno, or Bun #14164.
A fresh install reusing a warm global virtual store skips reimporting packages whose target directory is already complete #11112.
A warm pnpm install reuses on-disk package metadata for five minutes when the registry does not send an ETag. Registries that send an ETag, including the public npm registry, still revalidate with a conditional request. pnpm update still fetches current metadata #13976.
pnpm install now repairs a pnpm-lock.yaml whose (patch_hash=<hash>) dependency paths disagree with its patchedDependencies map, including paths that lack the hash their patch calls for. pnpm previously accepted such a lockfile as up to date and kept the old patched files. pnpm install --frozen-lockfile now fails on such a lockfile with ERR_PNPM_INCONSISTENT_PATCH_HASH. It fails with ERR_PNPM_UNCHECKABLE_PATCH_HASH when a patch hash in the lockfile is malformed, or when the lockfile lacks the package version or patch entry that the check needs #15336.
pnpm install now fails with ERR_PNPM_PATCH_NOT_FOUND when a patch file listed in patchedDependencies does not exist. It used to fail with a raw ENOENT error and a stack trace #5268.
pnpm now fails with ERR_PNPM_INVALID_PATCHED_DEPENDENCY when patchedDependencies has an invalid shape or contains a non-string value.
engineStrict now checks the patched package.json when a patchedDependencies entry changes engines. A patch that relaxes engines.node no longer fails the install against the published range #9603.
pnpm patch now applies the existing patch file to the edit directory of a git-hosted dependency, as it already does for packages from the registry #9699.
pnpm patch-commit now fails with an error when git cannot be found in PATH. It previously reported that no changes were found #8666.
pnpm install refreshes injected copies of workspace packages when source projects are rebuilt. Injected copies previously stayed stale until pnpm install --force #4407.
Scripts listed in syncInjectedDepsAfterScripts now update injected dependencies while they run. A watcher on the injected package, such as a dev server, sees each change before the script exits #4410.
pnpm install no longer fails for an injected workspace dependency whose package publishes from a publishConfig.directory that its own prepare script builds. The injected copy now picks up that directory once prepare finishes building it. pnpm install --frozen-lockfile no longer reports the dependency as outdated while the directory has not been built yet #7811.
With sharedWorkspaceLockfile: false, an injected workspace package that has lifecycle scripts is now hard linked into the projects that depend on it. Before, pnpm left a plain copy, so later edits to the package did not reach those projects #9828.
injectWorkspacePackages now hard links a workspace dependency declared with a relative path, such as workspace:../foo, the same way it already does for workspace:* #10446.
pnpm install no longer creates a node_modules symlink inside the publishConfig.directory of a workspace package linked with linkDirectory. A build tool that cleaned its output directory through that symlink deleted the files of the package's dependencies. pnpm install also removes a symlink that an earlier install left there #16226.
When verifyDepsBeforeRun triggers an install before a filtered pnpm run or pnpm exec, pnpm now installs only the selected projects and their dependencies. A later filtered command also installs a selected project that an earlier filtered install skipped #11865.
pnpm import in a workspace now keeps the versions pinned by a yarn.lock inside a workspace project #4385.
Installing with pnprServer set now records the pnpmfile checksum in the lockfile, so a later pnpm install --frozen-lockfile accepts that lockfile. A frozen install through the pnpr server now fails if the pnpmfile changed. If the pnpmfile defines a readPackage, afterAllResolved or preResolution hook or custom resolvers, pnpm resolves dependencies locally and prints a warning that the pnpr server was not used #14460.
Installing through a pnpr server now links a workspace project at the directory its publishConfig.directory names. An install that resolves through a server which does not forward the setting fails with ERR_PNPM_PNPR_PUBLISH_DIRECTORY_MISMATCH. The server rejects a publishConfig.directory that points outside its project.
With autoInstallPeers, pnpm add and pnpm remove in a workspace project keep the locked version of a peer dependency the project declares. In a workspace where another project depended on a different version of that package, the peer could switch to that version #11225.
Adding a dependency now keeps unrelated transitive dependencies on their locked versions #11456.
pnpm update --recursive <pkg> no longer changes the version of a peer dependency that another workspace project installs automatically. Such a peer could move to a version outside the range the project declares, for example to React 19 in a project that declares react: ^18.3.1 #14928.
pnpm update package@beta on a dependency declared as catalog: now updates the catalog entry and keeps catalog: in package.json. Before, pnpm wrote the resolved specifier to package.json #13399.
pnpm update now applies an override that references a catalog with the catalog's new value when the update bumps that catalog entry. Before, the packages the override targets kept the old version in the lockfile #12159.
pnpm add <dir> now warns when the added directory declares peer dependencies, as pnpm link does. The directory is saved as a link: dependency, and its peers are not resolved from the project that adds it. Use the file: protocol to have them resolved #5523.
pnpm run and pnpm exec no longer install dependencies automatically when the root package.json still keeps overrides, packageExtensions, patchedDependencies, or ignoredOptionalDependencies in its pnpm field. pnpm no longer reads that field, so the install rewrote the lockfile without those settings. The command now fails and asks to move the settings to pnpm-workspace.yaml #16278.
pnpm run no longer reinstalls dependencies when a node_modules directory installed outside CI is used with CI=true, or the other way around #12337.
pnpm no longer hangs after a lifecycle script exits while a process it started in the background keeps the script's output open. pnpm stops reading that output one second after the script exits #5730.
A signal sent to pnpm, such as SIGTERM, now reaches the pnpm that pnpm switches to because of packageManager or devEngines.packageManager, and the one that pnpm with runs. The signal used to be dropped, so scripts running under that pnpm never got to shut down #9948.
pnpm run exits with the code of a script that handles Ctrl+C and shuts down. A script that finished cleanly is not reported as a lifecycle failure. The commands after it in the same script still run #9945.
pnpm run and lifecycle scripts use the configured scriptShell, including Git Bash on Windows, when shellEmulator is also enabled. shellEmulator still runs scripts when scriptShell is not set. Extra arguments passed to pnpm run are quoted for the shell that runs the script, so a Windows path stays intact #14719.
pnpm -r run /regexp/ now honors the tasks dependsOn declared for each script the selector matches, like running the script by name does. Matched scripts that depend on each other run in order. Each matched script runs once #15596.
Commands run from a POSIX shell through a dependency's own node_modules/.bin, such as node_modules/vite/node_modules/.bin/esbuild, no longer fail with MODULE_NOT_FOUND #10189.
pnpm rebuild with nodeLinker: hoisted no longer puts one package's parent node_modules/.bin directories on the PATH of the packages it builds after it.
pnpm publish now waits at least 5 minutes for the registry to answer a publish request, like npm. This fixes "409 Conflict - Failed to save packument" errors when the registry is slow to answer #11454.
pnpm deploy with a shared lockfile now copies workspace dependencies into the deploy directory, even when packageImportMethod is set to hardlink. Previously, their files were hard-linked to the workspace sources, so editing a source file also changed the deployed copy #12176.
pnpm deploy --prod no longer fails with ERR_PNPM_OUTDATED_LOCKFILE when the deployed project declares a devEngines.runtime with onFail: download. The runtime stays out of the deployed node_modules with the rest of the dev dependencies #15703.
pnpm deploy --legacy no longer leaves broken links to nested local dependencies of workspace packages #9575.
pnpm deploy no longer creates extra directories inside the deploy target and workspace projects when using a relative deploy path #10981.
Settings given on the command line, such as --registry and --store-dir, now take precedence over the values a pnpmfile updateConfig hook sets #14063.
An updateConfig hook that returns registriesByScope without the default or @jsr entry no longer crashes the install with Invalid URL. A missing default keeps the configured registry, and a missing @jsr falls back to the built-in JSR registry #15619.
The hook's registry and the default entry of its registriesByScope now set one default registry, which installs, pnpm publish, and pnpm login all use. If a hook changes both, registry wins. A route that is not a string fails with ERR_PNPM_INVALID_UPDATE_CONFIG_RESULT.
An updateConfig hook in .pnpmfile.cjs has to use config.registriesByScope, config.registriesByPrefix, and config.registryOptionsByUrl. These registry lookups were renamed in pnpm 11.23.0 from config.registries, config.namedRegistries, and config.registryOptions #15620.
An async updateConfig hook that resolves to undefined now fails with ERR_PNPM_CONFIG_IS_UNDEFINED, as a synchronous hook that returns undefined already did.
pnpm config set --location=project and pnpm config delete --location=project, run from a package inside a workspace, now write settings that belong in pnpm-workspace.yaml to the workspace root's pnpm-workspace.yaml. Before, they created a new pnpm-workspace.yaml in the current package, which made that package the workspace root. Settings stored in .npmrc are still written to the current directory #13757.
pnpm now reads the workspace directory override from PNPM_CONFIG_WORKSPACE_DIR, like other settings. NPM_CONFIG_WORKSPACE_DIR still works as a fallback #16275.
A ${VAR} placeholder in .npmrc or pnpm-workspace.yaml whose name matches a built-in object property, such as ${toString}, is now treated as an unset variable. It used to be replaced with the source text of a JavaScript function.
pnpm now rejects a falsy non-array packages field in pnpm-workspace.yaml, such as packages: false, with an error. It used to treat the field as omitted.
pnpm update --global now reinstalls the global packages that pnpm 10 installed into the previous global directory, <global-dir>/5, so their commands are linked into the pnpm home bin directory again and pnpm list --global lists them. Once every package is migrated, pnpm deletes the previous directory and the commands pnpm 10 linked into the pnpm home #11528.
pnpm env remove --global deletes Node.js versions that pnpm installed into its own store, including when another tool installed pnpm #8357.
pnpm self-update no longer suggests a downgrade when minimumReleaseAge holds back the registry's latest release. It now says that release is still within the cutoff #12006.
@pnpm/exe no longer ships a binary for arm64 musl Linux, such as Alpine on ARM. The published binary crashed with a segmentation fault at startup. Installing @pnpm/exe on that platform now fails with an error that suggests npm install -g pnpm or pnpm 12 #10443.
The macOS and Linux release archives no longer include Windows-only files, such as node-gyp.cmd and the fastlist executables #11352.
On Windows, pnpm run now passes the arguments after the script name to the script as typed. Before, cmd expanded %VAR% in them and backslashes arrived doubled. Line breaks still arrive as the two characters \n, because cmd cannot pass them. The command line pnpm prints for the script quotes the arguments the same way on every platform #16257.
On Windows, command shims now run tools whose paths contain non-ASCII characters #6999. The PowerShell shims do so in Windows PowerShell 5.1 too #16217.
On Windows, the .cmd command shims in node_modules/.bin now keep a % in the project path. Before, cmd.exe expanded it as a variable reference, so the command received a mangled NODE_PATH #15716.
Bin shims in node_modules/.bin run from Cygwin on Windows again. The shims passed a /cygdrive/c/... path to the Windows node found on PATH, so Node.js failed with Cannot find module 'C:\cygdrive\c\...' #12845.
pnpm setup no longer writes the pn.ps1, pnpx.ps1, and pnx.ps1 PowerShell wrappers. It also removes the ones an earlier setup wrote. PowerShell now runs pn, pnpx, and pnx through their .cmd wrappers, like pnpm itself. Before, these aliases failed with a "not digitally signed" error wherever the execution policy blocks unsigned scripts #8444.
On Windows, globally installed @pnpm/exe commands now run in the invoking PowerShell console and return their exit status #6503.
On Windows, installing @pnpm/exe with npm inside a project now writes node_modules/.bin shims that run the standalone executable #15688.
On Windows, pnpm env use -g and pnpm add -g node@runtime:<version> now replace a node.exe in the global bin directory that is a broken symlink. Previously they failed with ENOENT #5411.
On Windows, pnpm expands nested %VAR% references in PNPM_HOME and the other directory environment variables it uses for its home, store, cache, state, and config directories. pnpm fails with an error when a %VAR% reference remains after expansion #13236.
On Windows, if the global bin directory is not in PATH and a PATH entry still contains an unexpanded variable such as %PNPM_HOME%, the error now names that entry. A variable referenced from the user Path must be set to a full path and stored as a plain string (REG_SZ) for the entry to expand #5283.
On Windows, pnpm setup no longer garbles non-ASCII characters in existing Path entries #6346.
On Windows, pnpm setup repairs the PNPM_HOME registry type left by older pnpm versions, even when the configured directory has not changed.
On Windows, the ERR_PNPM_BAD_ENV_FOUND error of pnpm setup now shows the value PNPM_HOME is currently set to. It used to show the directory pnpm wanted to set.
On Windows, pnpm install no longer skips a dependency's build script on a later install when the script changes nothing inside the package directory #15667.
On Windows, pnpm now retries writing the workspace state file while another process, such as an antivirus scanner, briefly holds it open #14550.
pnpm licenses list now reports the actual on-disk package locations when using nodeLinker: hoisted or shamefully-hoist: true #8589. pnpm licenses list --json now includes every installed copy of a package in its paths array, including hoisted copies and isolated installations with different peer dependencies. Its paths also exist on disk when the isolated linker uses a custom modulesDir.
pnpm list now shows the correct path of a link: dependency that points to a directory on another drive on Windows. The path used to be appended to the project directory, such as C:\project\D:\lib, and pnpm list --long could not show the package's details #10362.
pnpm root now prints the configured modulesDir. It used to print node_modules regardless of the setting. A project's own modulesDir from packageConfigs is printed too #9113.
pnpm audit and pnpm audit signatures now fail with an error when the lockfile contains unresolvable dependency references #13638.
With the default and append-only reporters, installs with --loglevel warn or --loglevel error now print the full output of a failed install script. The output of successful scripts, including the root project's own install hooks, stays hidden. With --loglevel warn, pnpm also prints ignored build script warnings.
The lockfile verification error now suggests relaxing the policy that flagged an entry only if a fresh resolution still fails and you trust the affected packages. Errors from checks that no policy controls, such as a missing tarball integrity, no longer suggest relaxing a policy #14411.
pnpm run --recursive now prints GitLab CI collapsible sections that GitLab recognizes. The section markers used to appear as raw text in the job log.
Local tarball dependencies using the file protocol are no longer counted as downloaded in the progress banner #1103.
Nothing published for this version
pnpm update --latest no longer resolves past that specifier. pnpm audit --fix=update now warns when one of them pins a vulnerable version. The warning…
pn, pnpx, and pnx now run the pnpm installed alongside them. They used to look pnpm up on PATH. That failed when the directory holding them was not on PATH, and it silently handed the call to an unrelated pnpm when one came first there #14803.
The @zkochan/cmd-shim package is now available as @pnpm/bins.cmd-shim.
pnpm cache list-registries now prints the registry URL, matching pnpm cache view. It printed https%3A+registry.npmjs.org before and prints https://registry.npmjs.org/ now #15046.
pnpm deploy no longer installs the dependencies of the workspace root project into the deploy directory #6437.
Install warnings no longer carry the text of a package's deprecation notice. The warning names the deprecated package and version, and the pnpm:deprecation event no longer carries the notice either. pnpm view still shows it on request.
A deprecation warning now names the newest version of the package that is not deprecated, and says when reaching it means widening the range you declared:
WARN deprecated foo@1.0.0. 2.3.1 is not deprecated, outside the range you declared.
pnpm works this out from the metadata it already fetched, so it costs no extra request. An install that reuses the lockfile without fetching metadata names no version.
pnpm strips control characters from the package name and version in a deprecation warning, and from the notice pnpm outdated --long prints.
The text sanitizer now also strips the Unicode line and paragraph separators U+2028 and U+2029.
pnpm exec <command> and pnpm <command> run from a subdirectory of a project now find the executables installed in the project's node_modules/.bin. The command still runs in the subdirectory. PNPM_PACKAGE_NAME names the project #5068.
pnpm exec and pnpm dlx now wait for the command to finish shutting down after Ctrl+C. A signal sent to pnpm alone now reaches the command, the way it does with pnpm run. pnpm used to exit on the interrupt and terminate the command while it was still shutting down #7374.
Warnings about ignored environment variables in project .npmrc credentials now link to the npmrc documentation pnpm/pnpm#15051.
pnpm audit --interactive --fix=update no longer opens a second prompt for selecting dependencies to update #14927.
Fixed pnpm dedupe requiring a second pass after bumping a direct dependency in package.json pnpm/pnpm#14987.
pnpm deploy now writes plain versions for registry dependencies with peer dependencies in the deployed package.json. The deployed lockfile retains the resolved peer bindings. npm aliases keep their target package names #14873.
pnpm publish now allows a detached Git HEAD in CI, including checkouts of release tags. The working tree must still be clean. Branch and remote-history checks still apply when HEAD is attached pnpm/pnpm#5894.
pnpm dlx and pnx now prompt to approve dependency build scripts in interactive terminals. Cached packages with pending builds also prompt for approval. Without an interactive terminal, use --allow-build to allow the required builds. Fixes pnpm/pnpm#14943.
pnpm install --force now removes obsolete dependency links inside virtual-store packages when their dependencies change. Invalid dependency names are ignored during obsolete-link cleanup #15039.
pnpm add -g and pnpm update -g now ignore incomplete unrelated global package groups when every command from the replaced group is retained. Operations that could remove a global command still require complete ownership information.
Resolving a Node.js runtime now fails when unofficial-builds.nodejs.org cannot be reached. pnpm used to ignore that failure and leave the musl builds out of pnpm-lock.yaml. pnpm update then wrote a different lockfile on a machine whose network blocks the mirror pnpm/pnpm#14813.
pnpm now deduplicates a package whose child dependency resolved an optional peer in one workspace project but not in another. Two copies of next could appear when only some projects could reach styled-jsx's optional babel-plugin-macros peer #14800.
Fixed shell completion of package scripts for pnpm run and pnpm run-script pnpm/pnpm#15034.
Bash completion now preserves literal script names containing glob characters and shell punctuation in pnpm v11 and v12.
pnpm sbom now publishes a valid URL in the CycloneDX externalReferences[].url and the SPDX homepage. An npm shorthand such as vercel/ms or gitlab:group/subgroup/project is expanded to the git+https URL npm derives for it. An scp-style remote such as git@github.com:vercel/ms.git is expanded the same way. Any other URL is published in its normalized form, without embedded credentials. A value that names no repository, an email address for example, is left out. pnpm used to publish the raw value, so a shorthand produced a URL that consumers such as Dependency-Track reject pnpm/pnpm#14773.
pnpm setup now describes the displayed configuration changes as "the following configuration changes."
pnpm --version now reports why the pnpm version a project pins cannot be installed or recorded, then prints the version of the running CLI. It used to fail, which made the command unusable where the filesystem is read-only. pnpm --version also honors --store-dir and its --store alias now #14831.
pnpm install --force now reinstalls dependencies when the manifest and lockfile are unchanged. It previously reported "Already up to date" without reinstalling. Files changed in node_modules are restored when the store content is intact. Combining --force with --frozen-store now reports a configuration conflict on repeat installs #919.
pnpm install now installs git-hosted dependencies without preparing them when their builds are explicitly denied by allowBuilds. Dependencies that require preparation still need an explicit allow or deny decision pnpm/pnpm#10522.
pnpm runtime set and pnpm env use now use the pnpm version that started the command. They could run a different installed pnpm when the command was started through Corepack or another wrapper.
The install summary now names the version each dependency resolved to when node-linker is hoisted. It also lists what an install restores after node_modules is deleted, and both sides of a version change. The summary showed the range recorded in package.json, or nothing at all #15161.
The @pnpm/npm-lifecycle package is now available as @pnpm/exec.npm-lifecycle.
Fixed minimumReleaseAge making pnpm download a package's full metadata again on every install. The cached copy carried a validator the registry could not match, so pnpm could never revalidate it pnpm/pnpm#15103.
pnpm now measures a pnpm.overrides entry written as a bare path, such as ./local-dep, from the directory holding pnpm-workspace.yaml. It used to be measured from each package the override rewrote, so the dependency linked to a directory that does not exist #11131.
pnpm now preserves scalar YAML anchors and aliases when editing pnpm-workspace.yaml. Removing the entry that defines an anchor keeps surviving aliases valid. Entries updated to different values are written separately #8245.
pnpm now preserves comments and existing key order when updating package.yaml. New keys are appended to their mapping pnpm/pnpm#2008.
pnpm install --prod no longer downloads the registry packages that only a devDependency reaches #881.
pnpm update --global no longer reinstalls a global package when its dependency graph resolves to what is already installed. It reports Already up to date pnpm/pnpm#12002.
The minimumReleaseAge approval prompt now counts and displays each package version once pnpm/pnpm#15083.
pnpm run no longer sends a script a second SIGINT when Ctrl+C is pressed in a terminal. A script that shuts down on the first SIGINT and exits at once on a second used to die before its shutdown finished #7374.
pnpm now reads a pnpm-workspace.yaml whose tasks section uses a setting only pnpm 12 acts on, such as concurrencyGroup. A task's unrecognized fields are ignored, unless the field only differs in case from concurrency or dependsOn, which pnpm reports as a typo.
The warning about unrecognized top-level settings now names cargo, concurrencyGroups, and pipelines as pnpm 12 settings.
pnpm -r list --json now prints one JSON array. It printed a separate array for each project when sharedWorkspaceLockfile was false, so the output could not be parsed.
pnpm -r list now reads each project's own modules directory when the projects keep their own lockfiles, so --long and --parseable report the packages that project installed #15011.
A signal sent to pnpm while it runs without a terminal, as a container runtime or a service manager does, now reaches the script even when the shell running it stays the script's parent. pnpm then waits for the script to finish shutting down. Such a signal used to end the shell at once or stay with it, and the script was never told to stop #7374.
Fixed minimumReleaseAge being skipped for packages served by a registry that returns the same ETag for abbreviated and full package metadata pnpm/pnpm#14925.
pnpm install now returns "Already up to date" in a workspace where dedupeDirectDeps left a project without a node_modules directory of its own. Such a project forced a full install on every run.
Installs in different projects that share a global virtual store no longer fail on Windows with Access is denied while repairing the same slot #15114.
pnpm sbom now emits a license value as a CycloneDX expression only when it is a valid SPDX license expression. Anything else is emitted as a CycloneDX license name pnpm/pnpm#14786.
A dependency's own bins can no longer take over another package's bin shim. The POSIX shims pnpm generates used to look up their shell helpers on PATH, where a dependency's bins come first #14837. Reinstalling replaces the shims already in your node_modules. On Cygwin, MSYS2, and WSL the shims still take their Windows path conversion from PATH, so a dependency can still redirect them there.
POSIX bin shims now convert a Windows-form path such as C:\node_modules\.bin\tsc correctly. The shim mangled the backslashes in such a path and could not reach the package it runs. Installing again replaces the shims already in node_modules #14867.
pnpm pack now writes tarball entries grouped by file extension and file name, the order npm uses. Packages that ship many same-named files, such as template collections, pack much smaller #14766.
A command run in a project that the workspace does not include now acts on that project alone. A project is outside the workspace when it has a manifest of its own and no pattern in the packages setting selects it, or when a ! pattern excludes it. A directory with no manifest of its own, such as a package's source directory, still belongs to the workspace. pnpm install in an excluded project used to install every project in the workspace #3561.
POSIX bin shims now take cygpath and wslpath from the system default path on Cygwin, MSYS2, and WSL2. The shims looked both helpers up on PATH, where a dependency's own bins come first, so a dependency could redirect another package's shim. Installing again replaces the shims already in node_modules #14866.
pnpm update and pnpm audit --fix=update no longer copy dependencies added by packageExtensions, a readPackage hook, or an override into package.json. Those dependencies keep the specifier the hook or override gives them. pnpm update --latest no longer resolves past that specifier. pnpm audit --fix=update now warns when one of them pins a vulnerable version. The warning points at pnpm audit --fix #14928.
Nothing published for this version
Nothing published for this version
This tag was signed with the committer’s verified signature .
This tag was signed with the committer’s verified signature .
zkochan Zoltan Kochan
GPG key ID: 649E4D4AF74E7DEC
Verified Learn about vigilant mode .
Added an opt-in proof of concept that 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.
Configure it with the new remoteSideEffectsCache setting. A workspace names the eligible organization and packages; everything describing the act of signing — publish, keyId, builderId, trustedKeys, privateKey and the provenance fields — is refused in pnpm-workspace.yaml and read from the global config file or the environment instead.
Added macOS and Windows x64 and arm64 support to remote shared build artifacts pnpm/pnpm#13771.
Added the audit.ignorePrune setting. When set to true, pnpm audit --fix removes ignored GHSA entries that no longer appear in the audit report.
Generalized the experimental shared-artifact protocol so candidates and signed payloads identify a discriminated subject. Dependency side effects use package and source-integrity subjects, while workspace tasks use project and task subjects.
This changes shared-artifact request bodies and signed payloads. A pnpr server and its clients have to be on matching versions.
pnpm init now pins the latest pnpm version, instead of the version of pnpm that ran the command. A project scaffolded by an outdated pnpm therefore no longer inherits that staleness through its own devEngines.packageManager / packageManager pin #7490.
The version is read from the latest tag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry, offline, or a latest that the minimumReleaseAge / trustPolicy settings reject — pnpm init pins the running version as before, and never fails or hangs on the lookup. A latest that is older than the running pnpm is never pinned either.
A scope set in a project's pnpm-workspace.yaml is now ignored, with a warning naming where to set it instead. pnpm login records the scope as a @scope:registry route in the machine-global auth.ini, which outranks ~/.npmrc in every project — so a repository-committed file could redirect a scope such as @acme for all of a user's other projects after one routine login. Use --scope, the PNPM_CONFIG_SCOPE environment variable, or the global config file instead #13557.
Verified remote build artifacts are persisted in the shared store with their signed origin metadata. Later installs reverify the artifact against current trust, policy, platform, and source before reuse, while invalid remote variants are quarantined per channel (pnpm/pnpm#13771).
Persist completed recursive tasks so --resume-from skips exactly the work that passed during a matching interrupted or failed pnpm -r run / pnpm -r exec invocation. When no compatible state exists, pnpm retains its graph-based resume behavior.
Allowed pnpm update --patches to refresh registry revisions through a configured pnpr server while retaining locked package versions.
Added explicit registry revision selection with <version>+rN and pnpm update --patches for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries.
Workspace install, rebuild, pack, publish, stage, and lifecycle work now starts as soon as its dependencies finish instead of waiting for an unrelated topological group.
pnpm stage approve now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. 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 selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry.
Added per-task concurrency limits to workspace task orchestration. Set tasks.<name>.concurrency in pnpm-workspace.yaml to limit how many instances of that task may run across workspace projects at once:
tasks:
build:
concurrency: 2
Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the registries setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts.
sideEffectsCache now declares the whole of how a package's build output is reused — whether one is restored, whether one is saved, and the remote tier that shares it between machines:
sideEffectsCache:
read: true
write: true
remote:
org: acme
packages: ['native-addon']
sideEffectsCache: true, sideEffectsCacheReadonly, remoteSideEffectsCache, and its organization field all keep working. Where a field is set under both spellings the one above wins; where it is set under only one, it is kept.
Two behaviors change, both bringing this CLI in line with what the Rust one already did: sideEffectsCacheReadonly: true now blocks writing to the cache, and setting it alongside sideEffectsCache: false gives a read-only view rather than switching the cache off entirely. A cache can also be declared write-only now, to populate one the run does not read.
Workspace task orchestration (pnpm/rfcs#23). pnpm -r run and pnpm -r exec now schedule per task instead of in topological chunks: a task starts as soon as the tasks it depends on have finished, so a project no longer waits for unrelated projects that happen to share its chunk.
A new tasks section in pnpm-workspace.yaml declares what a task depends on, using the ^ convention:
tasks:
build:
dependsOn: ['^build']
test:
dependsOn: ['build']
lint: {}
^name means the named task in each of the project's workspace dependencies; a bare name means the task in the same project; an entry with no dependsOn declares an empty dependency list. A task with no entry behaves as dependsOn: ['^<its own name>'], which is exactly what the previous chunked ordering implied — an unconfigured workspace gets the scheduler improvement and nothing else changes meaning. A project without the script is reported skipped and passes its edges through to its own dependencies, so a scriptless package does not sever a chain.
Also part of this change:
ERR_PNPM_TASK_CYCLE) instead of silently running in an arbitrary order. Setting ignoreWorkspaceCycles: true downgrades the error to a warning: the cycle's tasks run in an arbitrary order relative to each other.--resume-from now skips exactly the transitive dependencies of the anchor package; work unrelated to the anchor still runs.--no-bail, tasks whose dependencies failed are reported as skipped, not failed, and do not add to the exit code.--bail (the default), the first failure still ends the run at once and nothing new is dispatched — including scripts already queued behind the concurrency limit.pnpm -r run --dry-run <script> prints the task graph that would execute without running anything (including skipping the verifyDepsBeforeRun check); --json emits the tasks and their resolved dependency edges.--workspace-concurrency=1, or the graph forces the scripts to run one after another).An _auth entry in the global config file no longer decides which registry packages come from when something else says. A registry or registries declared in pnpm-workspace.yaml or the global config now wins over the route inferred from a stored credential, which still applies where nothing else declares one. The pnpm_config__auth environment variable is unchanged: it stays the way to point a CI runner at a mandated proxy, and still overrides what a repository declares.
Prevent installs through a symlinked node_modules directory from rewriting the target checkout pnpm/pnpm#14286.
Treat empty scripts selected by a regular expression as missing before running dependent tasks.
The options type of the fetch command now declares allowBuilds, a setting its handler already forwarded to the installer. Type-level only — what pnpm fetch does is unchanged.
Filter hidden scripts matched by a regular expression during recursive runs when a visible script also matches.
Fixed automatically switched pnpm versions forcing all descendant pnpm processes to use the same version pnpm/pnpm#14309.
Fixed ERR_PNPM_UNUSED_PATCH validation during incremental installs pnpm/pnpm#13692.
Fixed pnpm deploy --prod failing when an excluded dev dependency was also declared as an optional peer dependency pnpm/pnpm#14302.
pnpm update -g no longer downgrades a global package. --latest resolves the latest dist-tag, which can point at an older release than the one installed — after pnpm add -g <pkg>@next, for instance #14270.
pnpm update -g also no longer changes the pnpm version. pnpm's own global install belongs to pnpm self-update #14270.
Copying a built package to its other hoisted locations no longer replaces the destination directory. With nodeLinker: hoisted, that replacement deleted the dependencies nested inside the destination's node_modules, and made concurrent copies of the same build chunk fail with ERR_PNPM_ENOENT: no such file or directory, rename '.../node_modules/_tmp_...' #12880.
pnpm update no longer replaces the specifier a project declares for a dependency that is also listed in overrides. A catalog: reference stays a catalog: reference, and a declared range stays as written, instead of being rewritten to the version the override resolved to #12115.
pnpm update no longer moves the range a project declares for a dependency that overrides also lists, even when the override repeats that range verbatim. Previously the updated package.json disagreed with the lockfile, so the next pnpm install --frozen-lockfile failed with a specifier mismatch #14224.
Make pnpm add --lockfile-only skip dependency linking pnpm/pnpm#14286.
--production is accepted again as an alias of --prod on install, fetch, prune, update, list, why, and sbom, and the install that verifyDepsBeforeRun reproduces is now spelled with --prod. pnpm run no longer aborts with "unexpected argument '--production' found" after a production-only install #14147.
The progress output no longer overwrites the lines above it once it grows taller than the terminal window #14270.
Restoring a dependency's build from the remote side-effects cache no longer downloads files the store already holds.
Forward patchedDependencies hashes and packageExtensions to pnpr so server-side resolution preserves patches and package extensions in the lockfile and installed packages.
Published the workspace task graph and scheduler as @pnpm/workspace.task-scheduler so other workspace commands can use the same dependency-aware scheduling as recursive run and exec.
The environment variables for the remote side-effects cache are named for the setting they configure: PNPM_SIDE_EFFECTS_CACHE_REMOTE_KEY_ID, ..._BUILDER_ID, ..._IMAGE_DIGEST, ..._ARCHITECTURE_BASELINE, ..._PRIVATE_KEY, ..._BUILD_ENV, ..._TRUSTED_KEYS and ..._PUBLISH. The PNPM_REMOTE_SIDE_EFFECTS_CACHE_* names keep working, and the new one wins when both are set.
A devEngines.packageManager range pin on pnpm is now recorded in pnpm-lock.yaml's packageManagerDependencies when the running pnpm already satisfies it, using the running version and keeping the range as the recorded specifier. Previously only an exact pin — or a range resolved on the way through a version switch — reached the lockfile, so a range pin written by hand (or by any tool other than pnpm add / pnpm self-update) left the project without the shared resolution the pin exists to provide.
Fixed recursive run cleanup on Windows when a lifecycle script fails while another script's process tree is still running.
The update notification now suggests pnpm self-update when PNPM_HOME manages the pnpm in use, and the standalone install script otherwise — under Corepack, or when another package manager installed pnpm. pnpm self-update under Corepack names the standalone install script too.
Enforce allowBuilds when a prepared git dependency is reused from the shared store, and use the lockfile's canonical git resolution ID in approval suggestions.
Topologically sorting workspace projects now runs in linear time, fixing installs and lockfile updates that stalled for seconds on workspaces with thousands of projects forming deep dependency chains #14149, #14151.
This tag was signed with the committer’s verified signature .
This tag was signed with the committer’s verified signature .
zkochan Zoltan Kochan
GPG key ID: 649E4D4AF74E7DEC
Verified Learn about vigilant mode .
cef4816
This commit was created on GitHub.com and signed with GitHub’s verified signature .
GPG key ID: B5690EEEBB952194
Verified Learn about vigilant mode .
Fixed pnpm v11 incorrectly reporting confirmModulesPurge as unrecognized when set in pnpm-workspace.yaml . The Rust CLI now identifies the unsupported option as a pnpm v11 setting instead of suggesting an unrelated setting.
pnpm install --frozen-lockfile no longer fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE when the pinned pnpm version recorded in pnpm-lock.yaml has to be re-resolved before it can be installed. It runs the pnpm version the lockfile pins and leaves the lockfile unchanged #14124 .
Under nodeLinker: hoisted , peer-resolution variants of an injected directory dependency (a file: snapshot) are materialized as separate copies again instead of collapsing onto the first-seen variant. Each copy keeps its own peer-resolved dependency set, so a project pinning one peer version no longer resolves another project's variant — Bit root components with conflicting peers across injected copies rely on this.
Fixed pnpm install --merge-git-branch-lockfiles --frozen-lockfile failing with ERR_PNPM_OUTDATED_LOCKFILE when a branch lockfile predates the removal of a dependency, or its move to another dependency group #13966 . A dependency that no project declares anymore is no longer reinstated by the merge, and the packages it was the only path to are dropped with it.
Batch workspace publishing accepts a shared scope-specific credential, rejects mismatched credentials for a registry before publishing, and runs the publish and postpublish scripts after each completed registry group pnpm/pnpm#14101 .
The Rust CLI now honors five settings it recognized but ignored: updateNotifier , legacyDirFiltering , initAuthorName / initAuthorEmail / initAuthorUrl , initLicense , and initVersion . pnpm install and pnpm add check once a day for a newer pnpm and print how to get it (turn it off with updateNotifier: false ); a {<dir>} filter selector can go back to matching the subtree below the directory with legacyDirFiltering: true ; and pnpm init writes the configured author, license, and version into the package.json it scaffolds. PNPM_CONFIG_INIT_VERSION is now read as well.
maxsockets , npm's spelling of maxSockets , is no longer ignored: both spellings are read from pnpm-workspace.yaml , the global config file, the environment, and the command line, in that increasing order of precedence — a value passed on the command line now wins even when the two sides spelled the setting differently.
A lastUpdateCheck timestamp dated in the future — after a clock change, a restored snapshot, or a hand-edited state file — no longer silences the update check until that time comes around.
legacyDirFiltering no longer reaches the workspace-root selectors pnpm generates for itself: the !{<workspace-root>} exclusion a recursive run / exec / add / test appends, and the {<workspace-root>} inclusion --workspace-root appends. Read as subtree matches they named every project below the root, so a recursive command under the setting selected nothing at all, and --workspace-root pulled in every project below the root instead of the root alone #14101 .
pnpm install --frozen-lockfile no longer fails when pnpm-lock.yaml records the pinned pnpm version alongside an engine package the running pnpm does not install it from. An entry pinning another version is still refused, and a plain install rewrites the block #14124 .
update and audit show the effective sections, whichever spelling set them. The deprecated internal spellings (updateConfig, auditConfig, auditLevel) a…
pnpm config get and pnpm config list now show the settings pnpm acts on under their documented names:
registries shows the registries pnpm resolves from, merged across every source (.npmrc, pnpm-workspace.yaml, the global config, CLI flags), in the shape the setting is written in: keyed by registry URL, with the default registry declared as the bare @ scope. Built-in routes are included — the @jsr scope and the npmjs and gh prefixes — unless pointed elsewhere. Previously pnpm config get registries printed undefined.update and audit show the effective sections, whichever spelling set them. The deprecated internal spellings (updateConfig, auditConfig, auditLevel) are no longer listed.catalogs shows the complete resolved catalog set — the singular catalog block is its default entry — whichever spelling declared it.registry and @scope:registry entries show the merged routes rather than raw .npmrc values, so they always agree with the registries view.Settings that no supported pnpm version recognizes get their own warning. A key in the global config file that this version of pnpm does not read is no longer reported with advice to move it to a project-level pnpm-workspace.yaml (where it would be ignored too); the warning now says the setting is not recognized by this version of pnpm, names the pnpm version that does read it when there is one (for example, globalShims is a pnpm v12 setting), and suggests the closest real setting name when the key looks like a typo. Unrecognized and non-camelCase keys in a project's pnpm-workspace.yaml, previously ignored silently, are now reported the same way. pnpm config get <key> and pnpm get <key> no longer print config-load warnings, so a script capturing the value gets the value alone.
The importPackage pnpmfile hook is deprecated. pnpm now prints a warning when a pnpmfile defines it, and the hook will be removed in the next major version. It also opts the installation out of the parallel package importer, making installation slower. If you rely on this hook, comment on #14101.
node_modules/.modules.yaml no longer records the registries an install resolved from, and the recorded copy is dropped from the file on the first install that rewrites it.
It dated from the lockfile format that spelled a dependency's path relative to its registry, where reading an installed tree meant knowing the registries it was installed with. Dependency paths have not carried a registry for several major versions, and the recorded copy outlived its use: pnpm list, pnpm why, and single-project installs preferred it over the project's own configuration, so a project whose registry had changed since its last install was still read through the old one.
They now use the configured registries, like every other command already did.
When enableGlobalVirtualStore is on, every process pnpm spawns for the project (pnpm run, pnpm exec, lifecycle scripts) now receives a NODE_PATH pointing at the project's hoisted node_modules, plus a NODE_OPTIONS --import flag that registers a resolve hook restoring NODE_PATH lookups for ESM imports. Dependencies that import undeclared ("phantom") packages keep resolving under the global virtual store — for both CommonJS and ESM — without installing the @pnpm/plugin-esm-node-path config dependency pnpm/pnpm#9618. Tools run by pnpm dlx resolve such dependencies too: the JS CLI passes them the same environment, while the Rust CLI's dlx cache is self-contained, so its layout already exposes them.
A registry can now declare that its abbreviated metadata carries the time field, so resolutionMode: time-based reads the full metadata document only from the registries that need it:
resolutionMode: time-based
registries:
https://npm.internal.example/:
supportsTimeField: true
registry.npmjs.org omits time from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: registrySupportsTimeField answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a time field npmjs does not serve. The answer is now per registry, and registrySupportsTimeField remains the answer for every registry that does not declare one.
The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf.
A pnpr resolve request now carries the client's registries the way the registries setting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one's serverType — in place of the prefix map it used to send.
The server routes them through the same inversion the config reader runs, so a pnpr-served install resolves a scoped dependency from the registry that scope is routed to, which it previously could not: only the default registry and the prefix-addressed ones reached the server. A declared serverType reaches it too, so the tarball URLs pnpr omits from the lockfile match the ones the client reconstructs.
Built-in scope routes the project has not pointed elsewhere are not declared, so a pnpr server's allowlist is not asked about npm.jsr.io on requests that resolve no JSR package.
A registry a request only declares is no longer refused up front for being off the server's allowlist — a client describes its whole configuration, including scopes a given resolve never reaches, so a stray @scope:registry in a developer's ~/.npmrc no longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message.
This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned.
The registries setting now declares a registry once, keyed by its URL, with everything about that registry in the entry: how it lays out tarball URLs, the scopes routed to it, and the bare-specifier prefix it answers to.
registries:
https://artifactory.example.com/artifactory/api/npm/npm-virtual/:
serverType: artifactory
scopes: ['@acme', '@acme-internal']
prefix: work
serverType tells pnpm how the registry lays out its tarball URLs, which decides whether a URL can be omitted from pnpm-lock.yaml:
npm — the registry behaves like registry.npmjs.org, which also serves a scoped package from its percent-encoded path. Declare this for a faithful mirror or caching proxy of the public registry so its tarball URLs can be omitted too.artifactory — JFrog Artifactory repeats the scope in a scoped package's tarball filename (@acme/widget/-/@acme/widget-1.0.0.tgz) where the npm registry strips it (@acme/widget/-/widget-1.0.0.tgz). Declaring it lets pnpm rebuild that URL, so it is omitted from pnpm-lock.yaml instead of being written out for every scoped package pnpm/get-npm-tarball-url#16.scopes lists the @-prefixed scopes that resolve from this registry. A bare '@' is the scope-less default registry, the one the registry setting names.prefix is the alias a dependency addresses this registry by, as in "foo": "work:^1.0.0".The layout is never inferred from the registry URL, so nothing changes unless you declare it; registry.npmjs.org continues to behave as npm without being declared. Because the lockfile depends on serverType, it is read from pnpm-workspace.yaml only — a serverType in the global config.yaml is ignored, so one developer's machine cannot shape a lockfile their collaborators read back with a different layout. Credentials are rejected in this setting, in a key as well as in a field, and still belong in .npmrc. An entry that routes nothing to itself and matches no configured registry is reported as a warning rather than silently ignored.
The older registries shape, a map of <scope>: <url> strings, still works and needs no change:
registries:
'@acme': https://npm.acme.example/
namedRegistries is deprecated in favor of the prefix field, and is still read for prefixes registries does not declare.
toLockfileResolution and isCanonicalRegistryTarballUrl now take their registry and layout as an options object rather than positional arguments, so @pnpm/lockfile.utils and @pnpm/resolving.tarball-url get a major bump.
An install that had to re-hash store files to verify them now reports it. If that cost more than a second, it says how long — The integrity of N files was checked in 2.5s. — and if it was quick but covered more than a thousand files, it names the cause instead: their timestamps changed since the store recorded them, which a backup tool, an antivirus scan or a copied store can do.
Added virtualStoreType, which names where the virtual store lives — one store per machine, or one per project:
virtualStoreType: global # or: project
It is the canonical spelling of enableGlobalVirtualStore, which keeps working. When a project sets both, virtualStoreType wins. It can also be set through PNPM_CONFIG_VIRTUAL_STORE_TYPE and read back with pnpm config get virtualStoreType. The default is unchanged — project, so the shared store stays opt-in.
The setting is independent of nodeLinker. isolated and pnp both work with either store type, and hoisted writes no virtual store at all, so it is unaffected.
pnpm add --allow-build now adds to the allowBuilds entries already in pnpm-workspace.yaml instead of replacing them #13872.
Kept pending build approvals available after removing an unrelated dependency.
pnpm approve-builds now removes onlyBuiltDependencies, onlyBuiltDependenciesFile, neverBuiltDependencies, and ignoredBuiltDependencies from pnpm-workspace.yaml when it writes allowBuilds. Those settings were replaced by allowBuilds in pnpm 11 and silently ignored since, so a workspace migrated from pnpm 10 kept them around looking active.
pnpm audit no longer reports a patched version that was never published or is deprecated. The inferred patched range (e.g. >=4.17.24 from <=4.17.23) is now checked against the registry packument, and the report is corrected to the lowest non-deprecated published version that satisfies it (e.g. >=4.18.1 when 4.17.24 does not exist and 4.18.0 is deprecated). When no published version satisfies the range, the report shows Patched versions: None. This also prevents pnpm audit --fix from adding overrides or minimumReleaseAgeExclude entries for patches that do not exist #13824.
pnpm audit --fix and pnpm audit --fix update no longer add a minimumReleaseAgeExclude entry when the registry packument shows that the minimum patched version was never published. Previously such entries were written for versions that do not exist, which would have let a later publish of that version bypass the minimumReleaseAge gate #11563.
The --json output of pnpm audit now returns patched_versions: null for advisories whose inferred patch is not available (never published, skipped, yanked, or deprecated), making it easier for tooling to distinguish "no fix available" from "fix available at version X".
Fixed pnpm patch-commit in project and edit paths containing non-ASCII characters.
The package and bump pickers of pnpm change now size their page from the terminal height instead of always showing 7 rows. They fall back to 7 rows when the terminal height is unknown pnpm/pnpm#13815.
Canceling a pnpm change prompt with Ctrl-c no longer prints a stack trace. It reports Change canceled and exits with a success status, like the other interactive commands #13814.
Re-fetch full registry metadata when minimumReleaseAge is enabled and an abbreviated packument's time map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version pnpm/pnpm#13741.
A config dependency carrying an inline integrity (the <version>+<integrity> form, or the object form without a tarball) now takes its tarball URL from the registry's packument instead of deriving it from the registry URL, so migrating one costs an extra metadata request. On a registry that serves tarballs from a path pnpm cannot derive, GitLab's group endpoint for one, installing such a config dependency failed with a 404 while the same package installed fine as a regular dependency #13765.
Fixed PNPM_CONFIG_NODE_VERSION being ignored when setting the Node.js version used for compatibility checks.
A custom fetcher can no longer replace the archive integrity that pnpm-lock.yaml pins: the locked value is restored after a canFetch or fetch hook rewrites the resolution, and delegating a locked archive to a directory or git source now fails instead of installing unverified content.
The Rust CLI now also loads the pnpmfiles named by the pnpmfile setting (a single path or an ordered list), and hands custom fetchers native localTarball and remoteTarball callbacks — including on a fresh install that has to compute a missing tarball integrity, which is then reused by later offline installs. File maps a fetcher returns are accepted only when they match what those native callbacks extracted.
Fixed an issue where running pnpm dedupe --check in projects with nodeLinker: hoisted would cause dependencies to be moved out of node_modules into node_modules/.ignored.
pnpm deploy --prod and pnpm deploy --no-optional no longer list the excluded dependency groups in the deployed package.json and pnpm-lock.yaml. The deployed lockfile referenced packages that the deploy left out of its graph, so installing in the deploy directory afterwards created dangling symlinks #13623.
Don't treat files like license16.json as a package license when deciding if the workspace LICENSE file should be included in the packed package.
pnpm exec --recursive --no-reporter-hide-prefix no longer prints a blank prefixed line after each chunk of a command's output, and no longer splits a line in two when it straddles a chunk boundary.
Fixed 404 errors when installing from a registry that serves scoped packages only from a percent-encoded path, such as GitHub Enterprise Server. Outside registry.npmjs.org, a tarball URL that encodes the scope separator as %2f or %2F is no longer mistaken for one that pnpm can rebuild from the package name, version, and registry, so it is kept in pnpm-lock.yaml and requested verbatim on the next install #13534.
Fixed trustPolicyExclude and minimumReleaseAgeExclude being ignored when set to a single string instead of a list. The value was read one character at a time, so the exclusion never matched the package it named — and a * anywhere in it matched every package, silently switching the policy off.
pnpm init now pins the exact pnpm version instead of a ^ range, and records it in the packageManager field alongside devEngines.packageManager. Corepack reads only packageManager and accepts nothing but an exact version, so it rejected the generated package.json with "expected a semver version" pnpm/pnpm#13969. A package created inside an existing workspace is still left unpinned — it follows the pin at the workspace root — and --no-init-package-manager still scaffolds a manifest without any pin. In pnpm 12, pnpm init also honors initType and its --init-type flag, so the manifest it writes is the same one pnpm 11 writes.
Fixed an issue where package overrides were written into the metadata cache, causing removed overrides to keep applying on subsequent installs pnpm/pnpm#13918.
On Windows, upgrading pnpm no longer leaves a stale pnpm.ps1 behind. PowerShell resolves pnpm.ps1 ahead of pnpm.cmd, so a shim written by an older installation kept running the previous version. Linking the pnpm CLI's bins now deletes it #13919.
Fixed an inconsistency where minimumReleaseAgeExclude (and trustPolicyExclude) wildcard/bare-name rules behaved differently in the evaluator and normalizer. A bare rule now consistently evaluates as matching every version, preventing unexpected behavior and silent widening of version policy exemptions when pnpm rewrites the workspace manifest pnpm/pnpm#13725.
A frozen install no longer rewrites the packageManagerDependencies block of pnpm-lock.yaml. When the pnpm version pinned by devEngines.packageManager (or by packageManager) is missing from the lockfile or no longer matches it, --frozen-lockfile now fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE instead of resolving the version and saving it, so a manifest whose pin was bumped without regenerating the lockfile can no longer pass CI #14009.
A git dependency installed over HTTPS from a hosted repository now keeps its branch, tag, or version range in the specifier recorded in package.json. It was written back without one, so the next pnpm update moved the dependency to the repository's default branch #13999.
Fixed pnpm update --global --latest failing with a 404 error when a globally installed package was not added from the registry by name. Packages installed from a local path (link:/file:), a git repository, a tarball URL, an npm: alias, or a named registry now keep their spec during a global update instead of being looked up by name in the default registry. See pnpm/pnpm#12854.
Fix recursive pnpm update <name>@<version> so an exact pinned update stays scoped to the requested version line: copies of the same package on another major line — or, for a 0.x request, another minor line — keep their locked resolution instead of being re-resolved along with the target.
Under nodeLinker: hoisted, a dependency declared against a peer-resolution variant of a package version is no longer dropped from the installed layout. All variants of a version share one hoisted copy, and edges pointing at any of them now resolve to it, so the depending project keeps the package in its .package-map.json and the depending package keeps it in its node_modules/.bin.
Fixed pnpm install --merge-git-branch-lockfiles deleting the per-branch lockfiles when the lockfile setting is false. Such an install never reads them, so it has nothing to merge them into and now leaves them alone.
Fixed pnpm install sometimes not exiting after printing Done in Xs #12297.
Fixed pnpm failing to read .modules.yaml files containing long dependency paths #13875. The manifest is now parsed as JSON (the format pnpm writes it in), falling back to the YAML parser only for manifests written by old pnpm versions.
With preferSymlinkedExecutables, NODE_PATH again points at the virtual store of the workspace root when pnpm is run from inside a workspace package, so scripts can resolve dependencies that live only in the hoisted store #13912.
Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win pnpm/pnpm#13976.
pnpm pkg get and pnpm pkg set now accept hyphens inside a dot-notation property path, so pnpm pkg get dependencies.some-package-name reads the key instead of failing with ERR_PNPM_UNEXPECTED_TOKEN_IN_PROPERTY_PATH. The bracketed and quoted forms already worked and are unchanged.
A resolve request now carries the client's resolutionMode, so an install delegated to a pnpr server picks versions the way the client would. time-based and lowest-direct reached the server as nothing at all, leaving it on its highest default: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored.
This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving highest; the protocol is still experimental and unversioned.
Fixed pnpm installs using pnpr to honor the client's autoInstallPeers, dedupePeers, and excludeLinksFromLockfile settings pnpm/pnpm#13389.
pnpm remove now prunes undecided entries ("set this to true or false") from allowBuilds in pnpm-workspace.yaml when sharedWorkspaceLockfile: true and the corresponding packages are removed pnpm/pnpm#13892.
Fixed workspace discovery for pnpm-workspace.yaml files without a packages field so commands only consider the workspace root instead of recursively scanning nested projects #14047.
A runtime installed through devEngines.runtime now matches the host when supportedArchitectures lists several platforms. Listing os: [darwin, linux] and cpu: [x64, arm64] used to install the runtime built for the first entry of each list, so a machine running Linux on arm64 got a macOS x64 Node.js that could not execute #13898.
pnpm sbom now fails with ERR_PNPM_SBOM_MISSING_IMPORTERS when pnpm-lock.yaml has no entry for a selected project, instead of writing an SBOM that under-reports that project's dependencies. Previously this crashed with Cannot read properties of undefined (reading 'devDependencies').
pnpm self-update now rewrites a simple devEngines.packageManager.version range (^/~) to the newly installed version, keeping the operator — matching how pnpm update and pnpm runtime set rewrite ranges. Complex ranges such as >=8.0.0 that the new version satisfies are still left unchanged #13935.
pnpm self-update <tag> no longer downgrades when the dist-tag points at the pnpm version already running and that version is younger than minimumReleaseAge. The maturity cutoff moved the tag back to the previous mature release, so pnpm self-update next-12 on v12.0.0-rc.4 switched to v12.0.0-rc.3.
pnpm set-script now updates package.json instead of failing with ERR_PNPM_NOT_IMPLEMENTED pnpm/pnpm#13956.
pnpm update now preserves the existing range operator when updating a prerelease dependency. See pnpm/pnpm#7002.
Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges ("*", "^1.2.3") rather than the workspace: protocol. With preferWorkspacePackages enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install.
Added fetchWarnTimeoutMs and fetchMinSpeedKiBps to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters pnpm/pnpm#12042.
An override change is now absorbed by the fast lockfile update even when another, unchanged override uses the catalog: protocol. Previously any catalog:-valued override forced a full re-resolution whenever the override list changed, which could move unrelated packages in the lockfile (for example after pnpm audit --fix added an override).
Packed workspace package manifests now preserve dependency order, making repeated pnpm pack output deterministic #10167.
pnpm update <name>@<version> now fails with ERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEP when the package is not a direct dependency of any selected project, instead of quietly updating it to whatever a fresh install would resolve. There is nowhere to record the version in that case, so the request cannot be honored, and the error points at the overrides entry that does pin a transitive dependency. Ranges and tags are unaffected, and a package that any selected project declares directly still takes its version as before.
trustPolicy: no-downgrade no longer aborts the install with ERR_PNPM_MISSING_TIME on registries that serve no per-version time field when minimumReleaseAgeIgnoreMissingTime is set. The trust check reads the same publish dates the minimumReleaseAge check does, so it now honors the same opt-in and skips the affected package with a warning #12446.
minimumReleaseAgeIgnoreMissingTime no longer lets a lockfile entry the registry does not list pass the minimumReleaseAge check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure.
The missing-time warning now names the check it is reporting on, so a package whose minimumReleaseAge and trustPolicy checks are both skipped warns about both instead of only the first.
pnpm update <pkg>@<version> now updates only the selected packages and leaves unrelated dependencies unchanged. A selector that renames the package it installs — pnpm update <alias>@npm:<pkg>@<version> or the jsr: equivalent — now targets the package the alias installs rather than the alias.
Fixed verifyDepsBeforeRun being ignored when set to install, warn, error, or prompt through the PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN environment variable or the --config.verify-deps-before-run flag #13816. Only the boolean values were accepted before, so a string value was silently dropped.
pnpm version <bump> with --dry-run no longer edits package.json files. It now only reports the bumps it would make, and skips the working tree check, the version lifecycle scripts, the commit, and the tag pnpm/pnpm#13953.
A lockfile entry whose resolution is unchanged no longer loses its recorded deprecated marker when a registry serves the package's metadata inconsiste…
Added pnpm cache path, which prints the directory pnpm uses for its metadata cache. CI setups can use it to cache that directory — including the lockfile verification log, which lets a job skip re-checking an unchanged lockfile against the configured supply-chain policies.
--config.config-dir no longer reaches the config through a project's pnpm-workspace.yaml, and neither do the --config. spellings of the other settings a project manifest may no longer contribute (--config.pnpm-home-dir, --config.workspace-dir, --config.global-pkg-dir, --config.root-project-manifest-dir). None of them was ever a supported way to set those directories: pnpm resolves them from the environment, and these flags took effect only because the project-manifest merge re-applied the command line afterwards. The dedicated flags, such as --dir and --global-dir, are unaffected #13629.
pnpm config set refuses to write a setting to a project's pnpm-workspace.yaml that pnpm does not read from there, rather than leaving a key in the file that does nothing. Those settings are configDir, pnpmHomeDir, stateDir and the others that name machine-level state. The command fails with ERR_PNPM_CONFIG_SET_NOT_A_PROJECT_SETTING, naming where the setting does belong when it belongs somewhere. pnpm config delete still clears one that a file already carries, in whichever spelling it uses #13629.
Added a new setting minimumReleaseAgeExcludePrune. When enabled, pnpm add, pnpm update, and pnpm remove prune the entries of minimumReleaseAgeExclude in pnpm-workspace.yaml that the freshly written lockfile no longer resolves: versions that are gone are dropped (an entry is removed once none of its versions remain), and entries for packages that are no longer in the lockfile are removed too. Name patterns (@scope/*) are always kept. The cleanup is skipped when the install's lockfile does not cover the whole workspace (sharedWorkspaceLockfile: false), since entries another project still needs would look stale.
Renamed cleanupUnusedCatalogs to catalogPrune, so that catalog pruning and release-age exclude pruning use one vocabulary. cleanupUnusedCatalogs continues to work; when both are set, catalogPrune wins.
A project's pnpm-workspace.yaml can no longer choose where pnpm keeps its credentials, its own installation, or the registry it downloads its next version from. One of those settings is configDir, which decided where pnpm login writes the granted token. bin, dir, globalBinDir, globalDir, npmrcAuthFile, pnpmHomeDir, stateDir, userconfig and workspaceDir are ignored there now too, and pnpm warns about the ones it finds. cacheDir and storeDir are unaffected #13629.
Resolving a Node.js runtime version (devEngines.runtime / runtime: specifiers) is now much faster: the per-version release metadata is cached in the pnpm cache directory after its signature is verified, and an exact stable version such as runtime:22.23.2 no longer downloads the Node.js release index. A pinned runtime whose metadata was fetched once resolves without any network access, which removes the noticeable delay on the first node invocation in a project pinning an already-downloaded runtime #13899.
Fixed intermittent ERR_PNPM_ENOENT and ERR_PNPM_ENOTEMPTY errors while renaming _tmp_* directories during installation with nodeLinker: hoisted, in workspaces that also use patchedDependencies.
pnpm add no longer re-resolves the dependency graph when pnpm-lock.yaml already holds a version satisfying the request — promoting a transitive dependency to a direct one, or adding to a second workspace package what a first one already depends on, now only saves the dependency in package.json and records its importer entry. A satisfying locked version is necessary but not sufficient: the install still falls back to a full resolution for a dist tag, an alias, a workspace:/catalog:/git/tarball specifier, --save-peer, an overridden package, a catalogMode other than manual, and — under resolutionMode: time-based or lowest-direct, which resolve a direct dependency to the low end of its range — a range several locked versions satisfy.
Global installs now switch over atomically. The command shims in the global bin directory point at a stable per-package link rather than at the directory a particular install produced, so pnpm add -g and pnpm update -g activate a new version by moving that one link instead of rewriting every shim. A command can no longer be missing from PATH while an install is in progress, and a failed install leaves the previous version in place.
pnpm audit --fix and pnpm audit --fix update no longer add minimumReleaseAgeExclude entries for patched versions that were published before the minimumReleaseAge cutoff. The publish time of each minimum patched version is now checked against the registry metadata, and only versions young enough to be blocked by the age gate get an exclusion entry #11563.
pnpm add <pkg>@<version> and pnpm update <pkg>@<version> under a non-manual catalogMode now move the catalog entry's resolution to the requested version. Previously, when the catalog entry was a range that covered the requested version but resolved to a different one, the request was dropped silently: nothing was installed, nothing was written, and no error was raised.
A project that wasn't part of an install that moved a catalog entry now follows the entry the next time it is installed. It used to keep the version the entry resolved to before — a version the entry no longer allowed — and no later install corrected it, so one catalog entry ended up resolved to two versions.
pnpm add <pkg>@<version> and pnpm update <pkg>@<version> under catalogMode: strict no longer fail with ERR_PNPM_CATALOG_VERSION_MISMATCH when the catalog entry is a range that the wanted version satisfies. The dependency keeps using the catalog; only a version that really falls outside the catalog's range is rejected #13715.
A changed catalogs or pnpm.overrides block no longer has to be the only change for pnpm install to update the lockfile in place. Editing an override while also removing a dependency, or changing a catalog entry in the same commit as a range bump, is now absorbed in one pass instead of re-resolving the whole dependency graph #13799.
Fixed the lockfile an in-place override update wrote when the overridden package was also a catalog entry: the entry kept the version it had before the override moved the package. The same could happen in reverse, when a catalog entry moved a package an override pins. Both cases now re-resolve instead.
pnpm install now updates the lockfile in place even when several kinds of changes happened since the last install — for example a removed dependency together with a widened ignoredOptionalDependencies list, or a dependency edit alongside a patch or settings change. Previously any combination of changes forced a full re-resolution #13763.
pnpm deploy injects workspace dependencies again, so the deploy directory is self-contained instead of symlinking back into the source workspace #13754. Enabling injectWorkspacePackages with dedupeInjectedDeps disabled now also rewrites already-linked workspace dependencies to injected copies.
pnpm deploy --no-optional no longer writes a lockfile whose snapshots reference optional dependencies that the deploy excluded.
Removing the last dependency that references a catalog entry via the fast lockfile update no longer leaves the stale catalog entry in pnpm-lock.yaml.
A git dependency whose clone (or shallow fetch) fails now reports which package it belongs to, under the ERR_PNPM_GIT_FETCH_FAILED code, with credentials in the repository URL redacted. When the lockfile records an SSH remote, the error also explains that fetching it needs an SSH key for that host, and that a lockfile entry written before pnpm v11.21 can be re-recorded over HTTPS with pnpm update <package> #13743.
An integrity recorded on a git dependency's resolution (resolution: {type: git, repo, commit, integrity: sha512-…}) is no longer treated as a checksum. pnpm never verifies a git checkout against such a hash — the commit pins the content — so it is now dropped when the lockfile is rewritten, and pnpm sbom no longer republishes it as a CycloneDX/SPDX checksum. Lockfiles carrying one also load again instead of failing with ERR_PNPM_BROKEN_LOCKFILE #13042.
pnpm sbom now also publishes the checksum of a type: binary runtime archive, which pnpm does verify.
A git dependency whose git ls-remote fails now reports the ERR_PNPM_GIT_RESOLVE_FAILED code, naming the dependency instead of printing a bare git invocation, with credentials in the repository URL redacted. A specifier that does not ask for SSH resolves over HTTPS, because the URL recorded in the lockfile has to work on every machine that installs it, so the error explains how to substitute the transport on a machine that can only reach the host over SSH (git config --global url."git@<host>:".insteadOf "https://<host>/") #13743.
A missing git executable is reported as one, instead of surfacing the raw failure to start the process.
Credentials embedded in a git specifier are redacted from the "Could not resolve <ref> to a commit of <repo>" errors too.
Resolving a public repository makes one git ls-remote round-trip instead of two.
pnpm install after moving a dependency between dependencies, devDependencies, and optionalDependencies now updates the lockfile in place instead of re-resolving the whole dependency graph #13696.
syncInjectedDepsAfterScripts no longer fails with ERR_PNPM_UNSUPPORTED_INODE_TYPE when a workspace package contains an inode that is neither a file nor a directory, such as the FIFO 1Password's environments create for .env. Such an inode cannot be hardlinked into the injected copy, so it is skipped and the rest of the package still syncs #13550.
syncInjectedDepsAfterScripts also no longer fails with EEXIST when a workspace package replaced a file with a directory of the same name since the injected copy was last synced.
syncInjectedDepsAfterScripts no longer fails with ENOTDIR when a workspace package replaced a directory with a file of the same name and the injected copy still held that directory's contents.
syncInjectedDepsAfterScripts now removes the bin link of a bin the script dropped. Previously only new bins were linked, so a build step that stopped declaring one left its shim behind, pointing at a command that was no longer there.
syncInjectedDepsAfterScripts now identifies a file by its device as well as its inode number. An inode number is only unique within one filesystem, so on its own it could match an unrelated file on another device and leave that path stale in the injected copy.
pnpm store prune no longer deletes the lockfile verification log. The log records which lockfile passed which supply-chain policies, so it stays valid across a prune of the store; keeping it lets the next install skip re-verifying an unchanged lockfile.
Widening a dependency's range no longer leaves the project on an older version. The lockfile update now points the project at the highest version of that dependency already in the lockfile that satisfies the new range — matching what a full resolution records — instead of keeping the locked version whenever it happened to satisfy, which could leave a duplicate behind. A range change that only an already-locked version satisfies is now also handled without re-resolving #13778.
resolutionMode is no longer ignored when minimumReleaseAge is in effect. lowest-direct and time-based pick the lowest satisfying version of a direct dependency again; previously any active release-age cutoff — including the built-in default — silently forced the highest, so resolutionMode only worked when minimumReleaseAge: 0 was set explicitly #13752.
Adding a package to a workspace no longer forces a full re-resolution when every dependency it declares is already locked for a sibling. The lockfile update writes the new project's importer entry from the versions the lockfile already holds; a dependency no locked version satisfies still reaches the resolver #13696.
pnpm config delete <key> no longer fails with ENOENT when the config file it would edit does not exist. Clearing a setting that was never set is a no-op #13651.
Changing a pnpm.overrides entry to a version range now updates the lockfile in place when a version the lockfile already holds satisfies the range, instead of re-resolving the whole dependency graph. Only exact versions were handled before #13696.
Changing a parent-scoped pnpm.overrides entry ("parent>child": "2.0.0") now updates the lockfile in place instead of re-resolving the whole dependency graph. Only the named parent's dependency moves; every other package keeps the version it had #13795.
Removing a dependency, or moving one to another already-locked version, no longer re-resolves the whole dependency graph just because some package resolves a peer with the same name. The lockfile update now compares the peer suffixes against the exact name@version the removal severed, so a suffix that names a different — still present — version of that dependency is left alone #13781.
Projects with a pnpmfile now use the fast lockfile update paths: an unchanged pnpmfile (proven by the recorded pnpmfileChecksum) no longer forces a full re-resolution for removals, dependency group moves, compatible range changes, and the other in-place lockfile rewrites #13696.
A lockfile entry whose resolution is unchanged no longer loses its recorded deprecated marker when a registry serves the package's metadata inconsistently — re-resolving to the same version keeps the deprecation instead of silently dropping the line #13846.
pnpm prune is now recursive by default inside a workspace, just like pnpm install. This fixes pnpm prune --prod in a workspace root emptying the node_modules directories of the other workspace projects, dropping the links to the workspace packages they depend on in production #13718.
A setting written in kebab-case in the global config.yaml is now reported instead of being silently ignored #13650.
pnpm remove no longer re-resolves the dependency graph. The removed dependency's entries are dropped from pnpm-lock.yaml and anything they made unreachable is pruned, without registry access. The install still falls back to a full resolution when a surviving package resolves a peer dependency through the removed one.
Removing a package from a workspace no longer forces a full re-resolution. The lockfile update drops the departed project's importer entry and prunes whatever only it depended on. A project that is still linked from a surviving project continues to be reported as an error #13696.
An install sharing a global virtual store no longer removes an incomplete package directory that another importer is still writing, which could fail with failed to remove existing directory ... prior to swap: Directory not empty. Such a directory is now repaired in place, and a package file left damaged by an interrupted install is restored instead of being kept.
pnpm sbom no longer emits components for optional platform-specific dependencies that cannot be installed on the current platform (for example, the native @rolldown/binding-* variants for other operating systems). Such packages are present in the lockfile but are never downloaded, so their license (and other metadata) could not be resolved and they appeared in the SBOM without one. pnpm sbom --lockfile-only still describes the whole lockfile graph, which is platform-independent by design.
An ssh:// git dependency pointing at a bracketed IPv6 host, such as ssh://[::1]/repo.git, is resolved now. Its colons were read as an SCP-style path separator, which turned the address into [:/1] and left the specifier unresolvable. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, an ssh:// git dependency written without user info — ssh://git.example.com/team/repo.git, git+ssh://git.example.com:2222/team/repo.git — no longer fails with TypeError: Cannot read properties of undefined (reading 'includes'). Only the user@host form worked before.
packageExtensions is now validated when the configuration is read, so a malformed entry (for instance a dependency range set to null) fails with an actionable error instead of crashing later during peer dependency resolution #13756.
Projects using resolutionMode: time-based now benefit from the fast lockfile update paths. A removal, a dependency group move, or a compatible range change no longer forces a full re-resolution just because the lockfile carries a time field #13696.
An install that drops the last dependent of a patched package no longer updates the lockfile in place and succeeds silently. Removing a dependency, widening ignoredOptionalDependencies, or adding a removal override could each prune the package while the patch stayed configured; such an install now falls back to a full resolution, which reports the unused patch with ERR_PNPM_UNUSED_PATCH. Under allowUnusedPatches, where the lockfile update is kept, the same install now warns that the patch went unused instead of saying nothing #13827.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
This tag was signed with the committer’s verified signature .
zkochan Zoltan Kochan
GPG key ID: 649E4D4AF74E7DEC
Verified Learn about vigilant mode .
Added interactive group selection to pnpm update --global --interactive.
Added interactive group selection to pnpm update --global --interactive.
Running pnpm setup, pnpm self-update, or a command that modifies the global installation (such as pnpm add --global) through sudo now prints a warning. pnpm keeps global packages and configuration in the invoking user's home directory, so running these commands as root silently operates on the root user's home directory instead of yours. They will fail with ERR_PNPM_SUDO_NOT_SUPPORTED in pnpm v12. Read-only global commands (such as pnpm bin --global) are unaffected.
Fixed pnpm failing to start under asynchronous Node.js module loaders when no .pnpmfile.mjs exists pnpm/pnpm#11701.
Fixed minimumReleaseAge fallback for custom dist-tags so the selected version does not exceed the registry’s original tag target.
Removing a dependency from package.json and reinstalling no longer re-resolves the dependency graph. The importer's entry is dropped from pnpm-lock.yaml, anything it made unreachable is pruned, and a catalog entry that loses its last referent is removed — all without registry access. Installs still fall back to a full resolution when a package that stays resolves a peer dependency through the removed one, since that would change the surviving package's entry rather than only prune.
Changing a catalog entry to a different exact version no longer re-resolves the dependency graph. The package is replaced in pnpm-lock.yaml directly, reusing the same check the pnpm.overrides fast path applies: every locked dependency of the package must still satisfy the new version's manifest. Installs fall back to a full resolution when anything other than the catalog reaches the package — an importer that depends on it directly, or another package that depends on it — since the graph would then need both versions.
Fixed a CI regression where github:owner/repo dependencies (and other shorthand Git specifiers) would fail to install with Permission denied (publickey) on CI runners that lack SSH keys. The Git resolver no longer records an SSH URL unless the user explicitly wrote one (e.g. git+ssh:// or git@host:...):
429 Too Many Requests, so host throttling of CI runners is no longer mistaken for a private repository.git ls-remote access is now tried before SSH, so a public repository whose visibility probe fails still resolves to a portable HTTPS URL instead of an SSH URL that only works where SSH keys are configured.codeload.github.com, which would fail to download for a private repository); it stays a regular git resolution so installs can use ambient Git credentials such as credential helpers and tokens.Note that a private repository that is reachable both over authenticated HTTPS and over SSH now resolves to its HTTPS URL, where previous versions recorded the SSH URL.
Fixes pnpm/pnpm#13276.
<!-- cspell:ignore publickey -->
ng build and nuxt build now work under the global virtual store: pnpm's built-in compatibility extensions add the tslib dependency that @angular/build uses without declaring and the unplugin dependency that @nuxt/vite-builder v4 uses without declaring.
Fixed link: dependencies under enableGlobalVirtualStore so linked children are materialized and slots remain isolated by their resolved link targets.
An install that skips resolution because pnpm-lock.yaml is already up to date now reacts fully to packages the lockfile removed — for example after pulling a lockfile in which a dependency was deleted. The hoist layer is recomputed, so a package that became hoistable when a direct dependency was removed is hoisted, and pendingBuilds entries for removed packages are dropped instead of staying pending forever.
The held-back-update warning printed by pnpm update no longer fires when minimumReleaseAge is the actual reason a newer version was not picked. The warning's baseline now applies the same maturity cutoff as the pick itself, so it no longer wrongly attributes the hold-back to "your manifests and already installed dependencies" or recommends an override that would defeat the age gate. See pnpm/pnpm#13071.
Checking whether ignoredOptionalDependencies is up to date no longer reorders the configured patterns. The check sorted them in place, which could move an ! exclusion ahead of the pattern it excludes from and flip which optional dependencies were ignored.
Changing autoInstallPeers, dedupePeers, peersSuffixMaxLength, excludeLinksFromLockfile, or injectWorkspacePackages no longer re-resolves the dependency graph when the lockfile proves the setting cannot affect it: no package or project declares a peer dependency for the peer settings, and no project depends on a directory or on another workspace project for the link and injection settings. The new setting is recorded in pnpm-lock.yaml and the install proceeds from the existing resolution. Every other case still falls back to a full resolution.
Adding, editing, or removing an entry in patchedDependencies no longer re-resolves the dependency graph. Resolution never reads a patch — it only records the patch file's hash against the package it matches — so the install now rewrites the affected entries in pnpm-lock.yaml and materializes the patched package from the store instead. Installs still fall back to a full resolution when the patched package is reachable as a peer dependency, and when the new configuration would leave a patch unused while allowUnusedPatches is off, so ERR_PNPM_UNUSED_PATCH is still reported.
Resolving a private git repository no longer blocks on an interactive credential prompt: git ls-remote now fails fast with an authentication error when git has no credentials for the repository #13522.
Lockfile verification now honors offline mode by using cached registry metadata instead of reaching the registry. When the required metadata is not available locally, verification reports the same ERR_PNPM_NO_OFFLINE_META condition used by offline resolution.
POSIX shell shims now follow symbolic links before computing basedir, preventing execution failures when a shim is invoked via an external symlink on PATH #13405.
The automatic packageManager version switch works again on registries whose tarball URLs point at a different host than the registry itself (load-balanced feed proxies, Artifactory-style mirrors). Package-manager entries are now always recorded with integrity-only resolutions — the download URL is derived from the trusted bootstrap registry instead — and entries persisted in an invalid shape by an earlier pnpm are discarded and re-resolved instead of failing every command #13619.
Registries that serve no npm signature metadata (private mirrors and feed proxies commonly strip dist.signatures) no longer break the automatic packageManager version switch and pnpm self-update #13147. When the configured registry cannot provide a verifiable signature, pnpm now fetches the signature from registry.npmjs.org and verifies it against the same embedded npm keys over the installed integrity — which proves exactly the same thing. If no signature can be obtained from either source (for example, both are unreachable, or the registry publishes only a shasum), pnpm proceeds with a warning instead of failing, but only when the packages resolve through a registry configured in the user's own (non-project) configuration; the download stays pinned by the lockfile integrity, and a signature that exists but does not validate still fails the switch.
pnpm fetch, and any install run with virtualStoreOnly, no longer writes a .pnp.cjs loader under nodeLinker: pnp. These installs populate the virtual store without linking the project, so the loader would have claimed the project resolves out of a store it was never linked into. The importer links and node_modules/.package-map.json were already skipped; the PnP loader now follows the same rule.
Prevent pnpm from removing project files when modulesDir resolves to the project root.
Speed up installs after adding ignoredOptionalDependencies patterns by removing newly ignored optional dependencies and pruning packages that are no longer reachable without resolving the dependency graph again.
When a failed install re-copies a bin script from the store, rerunning pnpm install now reapplies the executable bit to the bin instead of leaving it non-executable #12742.
pnpm root -g and pnpm bin -g now print warnings to stderr instead of stdout, so their stdout stays a clean, machine-readable path. Previously, running either command with --global in a project that pins a package manager (e.g. via the packageManager field) printed a warning like [WARN] Using --global skips the package manager check for this project ahead of the path, breaking programs that capture the output as a path #13672.
In pnpm 12, pnpm root -g and pnpm prefix -g are now supported (they previously failed with ERR_PNPM_CLI_ROOT_GLOBAL_UNSUPPORTED / ERR_PNPM_CLI_PREFIX_GLOBAL_UNSUPPORTED), and the reporter output of dlx, create, config, sbom, with, store, prefix, root, and bin goes to stderr, matching pnpm 11.
pnpm setup no longer makes Node.js print a MODULE_TYPELESS_PACKAGE_JSON warning about dist/worker.js on every command. The package.json it writes next to a standalone executable now declares "type": "module".
pnpm update without saving no longer records a version that the manifest's range excludes. The kept range stays authoritative: a requested version outside it is skipped with a warning, and a requested range, a dist tag, or --latest resolves within it instead of past it. Previously each of these could write a lockfile entry that contradicted its own specifier, which the next pnpm install --frozen-lockfile rejected with ERR_PNPM_OUTDATED_LOCKFILE #12764.
pnpm version -r --json now outputs [] instead of human-readable text when no pending changes exist pnpm/pnpm#13217.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
Security fix. Affects projects using namedRegistries on pnpm 11.1.0–11.19.x. It is semi-breaking for those projects — see "If you use named registries…
Security fix. Affects projects using namedRegistries on pnpm 11.1.0–11.19.x. It is semi-breaking for those projects — see "If you use named registries" below.
The lockfile recorded no marker for which registry a package came from. Packages were keyed by name@version alone, and entry lookup went through refToRelative(ref, name), so a dependency you declared against one registry could be satisfied by an entry that was actually resolved from another. When two registries served the same name and version, both collapsed onto a single packages: entry and whichever resolved first decided the tarball every consumer got.
That is a package-substitution risk: a package you expect from your private registry could be installed from a different registry that publishes the same name and version, and the lockfile recorded nothing that would let you tell.
Packages resolved from a named registry are now recorded under registry-qualified keys (<name>@<registryName>:<version>, e.g. foo@work:1.0.0), so each registry gets its own entry and the lockfile pins which one a dependency came from.
The lockfile format version is unchanged. Registry-qualified keys appear only for packages resolved from a named registry, so a project that does not use namedRegistries sees no difference, and older pnpm versions keep reading the file.
Your next non-frozen install re-keys those entries, which shows up as a lockfile diff. Commit it — that diff is the fix being applied. Review it: an entry that moves to a registry you did not expect is worth investigating.
Everyone working on the project should be on this version or newer before you do. An older pnpm reads the re-keyed lockfile fine — frozen installs are unaffected — but it does not produce registry-qualified keys itself, so any install that updates the lockfile writes those entries back to the old shape, and the next install on a current pnpm re-qualifies them. The result is a lockfile that flips back and forth, and while it is in the old shape the project is exposed again. Because the lockfile format version is deliberately unchanged, pnpm cannot detect this and warn you about it.
There is no setting to keep the old behavior: the old shape is the vulnerability.
Tarball URLs that follow the standard registry layout are no longer written to the lockfile for named-registry packages; they are recomputed from the namedRegistries setting on demand.
To use named registries, map your aliases in pnpm-workspace.yaml:
namedRegistries:
work: https://npm.enterprise.example.com/
npmjs: aliasnpmjs: now resolves to https://registry.npmjs.org/ with no configuration, alongside the existing gh: alias for GitHub Packages. It pins a dependency to the public registry even when registry points elsewhere, such as an internal proxy:
{ "dependencies": { "left-pad": "npmjs:^1.3.0" } }
npm: cannot do this — it is the alias protocol (npm:<name>@<range>) and resolves through whatever registry points at.
If you mirror or proxy npmjs, point the alias at your mirror:
namedRegistries:
npmjs: https://npm.internal.example.com/
Built-in registry URLs are also the prefixes a lockfile's recorded tarball URL is matched against when pnpm verifies a package. Without the override, an entry whose tarball URL is on registry.npmjs.org is verified against the public registry rather than your mirror. This only affects lockfiles that record such URLs — a canonical URL for your configured registry is omitted from the lockfile and unaffected — and only when a tarball-URL, minimumReleaseAge, or trustPolicy check runs. Overriding the alias is the same escape hatch GHES users already have for gh.
Every alias the lockfile references must stay in namedRegistries: reading an entry whose alias is gone fails with ERR_PNPM_MISSING_NAMED_REGISTRY rather than silently falling back to the default registry, since that would fetch a different package. Renaming an alias re-resolves the packages that used it.
Named registry aliases that shadow a reserved dependency specifier prefix (file, link, workspace, runtime, npm, jsr, ...) are now rejected with ERR_PNPM_RESERVED_NAMED_REGISTRY_NAME instead of being silently shadowed by the corresponding resolver.
pnpm licenses and pnpm sbom now keep the two artifacts apart as well: license records carry the registry alias, and SBOM components carry the purl repository_url qualifier.
An empty http-proxy, https-proxy, proxy, or no-proxy value — from the .npmrc, pnpm-workspace.yaml, the CLI, or the HTTP_PROXY / HTTPS_PROXY / PROXY / NO_PROXY environment variables — no longer fails the install with ERR_PNPM_INVALID_PROXY. Empty settings read as unset, so a shell exporting HTTP_PROXY= disables the proxy, and an empty proxy= in the .npmrc no longer suppresses HTTPS_PROXY #13533.
proxy=false in the .npmrc or proxy: false in pnpm-workspace.yaml now turns proxying off instead of being read as a proxy host named false. false and null on https-proxy / http-proxy / no-proxy read as unset, and on the command line they are ordinary host names, since a flag carries its value verbatim.
The env lockfile no longer pins @pnpm/exe alongside pnpm when the wanted pnpm version is 12 or newer. From v12 the unscoped pnpm package is itself the native executable, so @pnpm/exe is not published for it and resolving it would fail. The engine identity check now verifies the native binary through whichever package ships it.
lexCompare and nerfDart are now published as @pnpm/text.ordinal-comparator and @pnpm/config.registry-auth-key. Use these instead of @pnpm/util.lex-comparator and @pnpm/config.nerf-dart.
Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
Dependency resolution is faster: package metadata is now filtered once per packument instead of once per dependency edge when minimumReleaseAge is active, and parsed semver versions and ranges are reused instead of re-parsed on every comparison.
Security: pnpm rebuild now refuses a lockfile whose packages key carries a path traversal in the package name (e.g. ../../../escaped@1.0.0), instead of running that package's lifecycle scripts and linking its bins in a directory outside the virtual store. Such a name is rejected with ERR_PNPM_INVALID_DEPENDENCY_NAME.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
pnpm login no longer requires an interactive terminal when the registry supports web-based login: without a TTY it prints the authentication URL (skip
pnpm login no longer requires an interactive terminal when the registry supports web-based login: without a TTY it prints the authentication URL (skipping the QR code and the "Press ENTER to open the URL in your browser" prompt) and polls the registry until the browser approval completes. Only the classic username/password login still fails with ERR_PNPM_LOGIN_NON_INTERACTIVE in a non-interactive terminal.
The save-prefix setting now accepts =: newly added dependencies are saved with an explicit = operator (=1.2.3) instead of the setting being silently treated as the default ^.
allowBuilds entries can now approve git-hosted packages that pnpm downloads as a tarball, such as github: dependencies (which are fetched from codeload.github.com rather than cloned), by their repository URL without the resolved commit hash. This matches the hashless git+ matching already supported for cloned git dependencies. For example:
allowBuilds:
"foo@git+https://github.com/org/foo.git": true
This approves the package whether pnpm clones it or downloads a tarball, so the entry no longer has to be updated every time the pinned commit changes. GitLab and Bitbucket tarball downloads are matched the same way. Approving or denying a specific resolved commit by its full tarball dep path continues to work.
pnpm outdated --include-github-actions no longer blocks on an interactive git credential prompt when a workflow uses a private action repo.
Prevented minimumReleaseAge from replacing latest with a SemVer-greater version than the registry tag target #13034.
Fixed empty bundledDependencies and bundleDependencies arrays causing nondeterministic lockfile changes. See pnpm/pnpm#13123.
The install summary no longer prints (X is available) when the registry's dist-tags.latest is still held back by the active minimumReleaseAge policy. The hint only ever names the actual latest tag, so an immature latest suppresses the hint instead of advertising the version pnpm just refused to install #11698.
pnpm update keeps the explicit = operator of an exact version pin: a dependency saved as =3.5.1 now updates to =3.5.2 instead of the bare 3.5.2. See pnpm/pnpm#13168.
Preserve a workspace dependency's link: entry when a run does not target it — e.g. pnpm update <other-pkg> (with or without --recursive), or a plain install after a root/catalog dependency change — with injectWorkspacePackages, instead of spuriously rewriting it to a peer-suffixed file: protocol. See pnpm/pnpm#10433.
Workspace dependencies declared with a relative path (e.g. "foo": "workspace:../foo") are no longer silently dropped from the workspace projects graph, so --filter selection and the topological order of recursive commands take them into account.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
Fixed an installed optional dependency being left without one of its own required dependencies. When a package reached through optionalDependencies is
Fixed an installed optional dependency being left without one of its own required dependencies. When a package reached through optionalDependencies is installable on the current system but one of its regular dependencies is not, a lockfile-based install skipped that dependency and installed the parent anyway, so importing the parent failed with MODULE_NOT_FOUND. The dependency is now installed, and an install-check warning reports the incompatibility. A dependency is still only skipped when every path to it is optional, or when the package that pulls it in was itself skipped #13286.
pnpm setup now appends PNPM_HOME and the global bin directory to the GitHub Actions environment files (GITHUB_ENV and GITHUB_PATH), so later steps in the same job can run pnpm add --global and other global commands #9191.
Added support for publishConfig.name, which publishes a package under a different name than the one its manifest carries in the workspace. It is for a project whose published name is already taken by a sibling project, which otherwise has to be renamed by a build step just before publishing. Only the published artifact is renamed — dependents, pnpm-lock.yaml, and release tooling keep addressing the project by its manifest name — and the new name reaches the packed manifest, the tarball filename, and everything that addresses the package at the registry: the already-published check of pnpm publish -r, its registry selection, and the release-planning probes of pnpm change status and pnpm version -r #13345.
pnpm self-update no longer takes any instruction from the project it is run in:
.npmrc or pnpm-workspace.yaml can no longer redirect the download or attach credentials to it, and the project's default .pnpmfile.(c|m)js is no longer loaded. Pnpmfiles from trusted sources (the pnpmfile setting, the global pnpmfile, config dependencies) still apply.minimumReleaseAge settings in pnpm-workspace.yaml no longer affect self-update. They still govern the project's own dependencies; for self-update the cooldown now comes from the built-in default, your global config, a PNPM_CONFIG_* environment variable, or a command-line flag. This fixes self-update failing inside a workspace that raises the cutoff while succeeding everywhere else, and stops a repository from either waiving the cooldown or keeping you on an outdated pnpm by raising it.trustPolicy settings and to ci: a project can no longer weaken the trust check that guards the pnpm download, nor re-enable the confirmation prompt that a CI run suppresses.When self-update refuses a version that is younger than the cutoff, an interactive run now offers to update anyway; non-interactive runs still fail. CI never prompts, even on a runner that attaches a TTY.
Fixed pnpm licenses list to report every version when the same package is installed under multiple aliases pnpm/pnpm#13438.
Sort pnpm dedupe --check snapshot changes for stable output across pnpm implementations.
Strip Unicode formatting characters from registry- and manifest-derived terminal output.
Speed up installs after compatible catalog or direct dependency range changes by retaining the locked version without resolving the dependency graph again.
Speed up installs after safe override changes by reusing unambiguous compatible dependency resolutions, pruning obsolete dependencies, applying independent replacements and removals together, and handling parent-scoped "-" overrides without full lockfile resolution.
Installing a local file: directory dependency with the global virtual store enabled no longer fails with TypeError: Cannot read properties of undefined (reading 'split') #13335.
Local directory dependencies — file: directories and injected workspace packages — now get a global-virtual-store slot of their own per project. They used to share one slot across every project that depended on a directory of the same name, so a project could end up linked to another project's copy of the dependency.
The Workspace column of pnpm update --interactive now falls back to the project's path when its name is only whitespace, as it already did for a missing or empty one — all three render an equally blank label otherwise.
Checking GitHub Actions dependencies for updates is now opt-in for every command. Neither pnpm outdated nor pnpm update reads the workflow files unless --include-github-actions is passed or update.githubActions is set to true in pnpm-workspace.yaml. Reading them runs git ls-remote against every referenced repository, which fails in environments where GitHub is not reachable the way pnpm assumes (a GitHub Enterprise Server, a custom certificate authority, or an offline network) #13254.
pnpm outdated accepts the --include-github-actions option too.
pnpm update --interactive now measures its table in terminal columns rather than in characters. A package name, workspace name, or version containing wide characters (CJK, most emoji) no longer knocks its row's columns out of line with the rest of the group, and a wide character in a version no longer aborts the command with Subject parameter value width cannot be greater than the container width #13357.
The Workspace column of pnpm update --interactive is more informative in two cases. A dependency outdated at the same version in several workspace projects is offered as one choice, since selecting it updates every project — that choice now names all of them instead of only the first. And a workspace project without a name is now labelled with its path rather than left blank, so several unnamed projects can be told apart.
An auto-installed optional peer is no longer hoisted at a version the workspace root's own dependency on that package excludes. resolvePeersFromWorkspaceRoot already made the workspace root's specifier decide which version a missing required peer is installed at; the optional-peer picker ignored it and always took the highest version present anywhere in the graph. In a workspace whose root pins postcss: 8.5.10, an importer that depends on webpack and declares no postcss of its own got postcss@8.5.22 hoisted for terser-webpack-plugin's optional postcss peer, leaving two postcss@8.5.x instances in the graph #13320.
overrides now also govern peers that pnpm auto-installs. Previously an override only rewrote dependencies declared in a manifest, so a peer nobody declares — installed because autoInstallPeers is on — resolved against its declared peer range and could bring in a second copy of the very package the override pinned. For example, with overrides: { react: npm:react@19.2.0 } and a lone lucide-react dependency, pnpm installed react@18.3.1; it now installs the pinned react@19.2.0 #13320.
Under resolvePeersFromWorkspaceRoot, a workspace root dependency declared with link: or file: (or the path form of workspace:, such as workspace:../pkg) now satisfies another project's missing peer dependency at the linked package's own version, instead of being hoisted as a path. Those specifiers are relative to the project that declares them, so the same specifier reached a different directory — or none — from the project the peer was hoisted into, leaving a broken link. The root now has the same authority over the peer as it has when it declares the package with a version range #13373.
Installs through a pnpr server now apply the project's whole verification policy. minimumReleaseAgeExclude, minimumReleaseAgeIgnoreMissingTime, trustPolicy, trustPolicyExclude, trustPolicyIgnoreAfter, and trustLockfile were ignored, so excluded packages were still held back and a lockfile containing them could be rejected.
trustPolicy: no-downgrade no longer fails with TRUST_POLICY_INCOMPATIBLE_WITH_PNPR when a pnpr server is configured.
--frozen-lockfile and --no-prefer-frozen-lockfile are now honored on the pnpr path, instead of resolving and rewriting the lockfile anyway. Since frozenLockfile defaults to true on CI, a CI install through a pnpr server now fails on an out-of-date lockfile rather than updating it.
Workspace installs through a pnpr server no longer crash with Cannot read properties of undefined (reading 'filter') after linking, when minimumReleaseAge is active #13275.
Fixed pnpm dedupe updating valid catalog resolutions when another matching version exists in the lockfile.
pnpm -r run "/pattern/" --no-bail no longer exits zero when one of a project's matched scripts fails and a later one passes. The run summary carries a single status per project, and the passing script overwrote the recorded failure.
Restored the store block a first install prints, naming how packages were materialized and where the stores live #13315:
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: ~/.local/share/pnpm/store/v11
Virtual store is at: node_modules/.pnpm
The root project's pnpm:devPreinstall script now runs before resolution and linking, as it does in pnpm 11. It is skipped under --ignore-scripts, --lockfile-only and --dry-run, by pnpm fetch and pnpm rebuild, and by a repeat install that is already up to date. Workspaces that use the hook to prepare state the install depends on — such as next.js, which generates a placeholder next bin with it — were left with dependents linked against files that were never created #13313.
Prevented pnpm dedupe --check from removing an incompatible node_modules directory.
pnpm update --workspace no longer links dependencies the user never named:
updateConfig.ignoreDependencies configured no longer fails with ERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUND for a dependency that is only published to the registry. Such dependencies keep their specifiers, as they already did when no dependencies were ignored.<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
Added a new setting, update.githubActionsServer, for specifying the base URL of the GitHub server that hosts the repositories of the GitHub Actions re
Added a new setting, update.githubActionsServer, for specifying the base URL of the GitHub server that hosts the repositories of the GitHub Actions referenced by the workflow files (for example, a GitHub Enterprise Server). When the setting is not defined, the URL is read from the GITHUB_SERVER_URL environment variable, falling back to https://github.com. The URL must use the https:// or http:// protocol #13220.
pnpm outdated and pnpm update no longer fail when the refs of a GitHub Action's repository cannot be read (for example, when the action's repository is private or hosted on a different GitHub server). Such actions are now skipped with a warning.
Setting update.githubActions to false now makes pnpm outdated and the interactive pnpm update skip GitHub Actions dependencies.
The token poll for web-based authentication no longer reads the body of non-OK or still-pending (HTTP 202) responses, and caps the token response body it does read at 64 KiB, so a malicious or compromised registry cannot exhaust memory through the poll pnpm/pnpm#12721.
Fixed catalog: references in dependencies and overrides failing to resolve when installing through a pnpr server, which errored with "No catalog entry '<name>' was found for catalog 'default'." even though the catalog entry existed. Also fixed a crash on Windows when installing a nested workspace member (e.g. packages/foo) through a pnpr server #13232.
Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug #13164.
Revert script ordering change for pnpm run --sequential /regex/
Support the from-git argument in the pnpm version command.
When the authentication URL cannot be rendered as a QR code (for example when it exceeds the maximum QR data capacity), web-based login now displays the URL alone with a warning instead of aborting authentication pnpm/pnpm#12721.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
The deprecated updateConfig, auditConfig, and auditLevel settings keep working until the next major version. When both a new section value and its dep…
The first release of a package now publishes the version written in its manifest verbatim, instead of bumping off it. pnpm version -r and pnpm change status check the registry for each release's current version; when that version is not yet published, the package debuts at it and its pending changesets apply only from the next release. A newly added package seeded at 1100.0.0 with a minor changeset is therefore published as 1100.0.0 rather than skipping straight to 1100.1.0.
Added a --changeset flag to pnpm update. Set update.changeset to true in pnpm-workspace.yaml to enable this behavior by default, and use --no-changeset to override the setting for one update. After the update completes, pnpm writes a .changeset/pnpm-update-<suffix>.md file declaring a patch bump for every workspace package whose dependencies or optionalDependencies were changed by the update and a major bump when peerDependencies changed, including packages that consume an updated catalog entry via the catalog: protocol. Private packages, packages without a name, and packages listed in the ignore array of .changeset/config.json are skipped. If .changeset/config.json does not exist, a warning is printed and no changeset is generated.
Added GitHub Actions dependencies to pnpm outdated and interactive pnpm update. Non-interactive updates can include them with --include-github-actions or by setting update.githubActions to true in pnpm-workspace.yaml. Updated actions are pinned to exact commit hashes with their release tags preserved in comments.
Added update and audit settings sections to pnpm-workspace.yaml, superseding the awkwardly named updateConfig, auditConfig, and top-level auditLevel settings:
update:
ignoreDeps: # was updateConfig.ignoreDependencies
- webpack
- "@babel/*"
audit:
level: high # was auditLevel
ignore: # was auditConfig.ignoreGhsas
- GHSA-xxxx-yyyy-zzzz
update.ignoreDeps lists dependency name patterns that pnpm update and pnpm outdated should skip. audit.level and audit.ignore tune pnpm audit.
The deprecated updateConfig, auditConfig, and auditLevel settings keep working until the next major version. When both a new section value and its deprecated counterpart are set, the new section takes precedence and a warning is printed. Both the TypeScript CLI and the Rust config surface (pacquet) recognize the new sections.
pnpm add --save-exact/--save-prefix and pnpm update writing a package's version with the peerDependencies range's prefix (e.g. ^19.2.7 instead of the requested 19.2.7) whenever the same package also appeared in peerDependencies. A real dependencies/devDependencies/optionalDependencies entry now takes precedence over a same-named peerDependencies entry when computing the current specifiers #13108.<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
pnpm install now detects a supportedArchitectures change and re-evaluates previously skipped platform-specific optional dependencies, instead of repor
pnpm install now detects a supportedArchitectures change and re-evaluates previously skipped platform-specific optional dependencies, instead of reporting the project as up to date and leaving the packages for the old architecture set in place.
pnpm setup now removes leftover v10-layout shims at the top of PNPM_HOME, so pnpm self-update no longer warns about a v10 installation layout after PATH has been migrated to the v11 PNPM_HOME/bin layout. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, self-update also no longer treats a dangling legacy shim (one whose install target was garbage-collected) as a real v10 layout, so the warning can no longer fire on dead shim files.
Closes pnpm/pnpm#12496.
Completed pnpm runtime installation parity for Node.js, Deno, and Bun, including runtime failure policy, target architecture selection, and dependency runtime engines. Runtime failure overrides now preserve explicit runtime dependencies without matching engine entries.
Fixed pnpm install running out of memory while resolving large dependency graphs #8441. The resolver kept full registry documents — per-version readmes, scripts, descriptions, and other install-irrelevant bulk — in memory for every package fetched with full metadata (optional dependencies, and packages re-fetched for minimumReleaseAge's publish timestamps). Every retained document is now condensed down to the field set installation actually reads, which reduces peak resolution memory by several times on workspaces with more than a thousand packages.
When a dependency's build script fails under enableGlobalVirtualStore, the global virtual store directory it was being built in is now removed for scoped packages too. Previously the cleanup resolved one directory level short of the hash directory for a scoped name, leaving a half-built directory behind that later installs would reuse.
Fixed pnpm login, pnpm adduser, and pnpm logout against a registry hosted under a URL subpath (e.g. https://example.com/npm/registry) when the configured URL has no trailing slash. Such URLs were left unnormalized, so the last path segment was dropped when building the login and token endpoints and the auth token was stored under a truncated key. Registry URLs with a path now always get a trailing slash appended during normalization, matching how root-level registry URLs are handled.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
Optional peer dependencies declared only via peerDependenciesMeta (for example debug's supports-color peer) are now resolved from a satisfying version
peerDependenciesMeta (for example debug's supports-color peer) are now resolved from a satisfying version already present in the dependency graph, the same way explicitly declared optional peer dependencies are. Previously such peers were only resolved this way when the package's metadata was read back from the lockfile, so an unrelated dependency change could rewrite peer resolutions across the whole lockfile.Updated adm-zip to prevent crafted ZIP archives from causing excessive memory allocation.
pnpm version -r no longer writes a versioning-ledger entry with no consumed intents as a bare intents: key, which the next run failed to read with ERR_PNPM_INVALID_VERSIONING_LEDGER. Empty intent lists are now written as intents: [], and the ledger reader accepts the bare form left by earlier releases.
Fixed pnpr workspace resolution to preserve project names and versions for workspace: dependencies.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
peerDependencies now accept dependency specifiers that carry a scheme — a named-registry spec ( : ), an npm: alias, or a file:/git/URL spec — instead
peerDependencies now accept dependency specifiers that carry a scheme — a named-registry spec (<registry>:<version>), an npm: alias, or a file:/git/URL spec — instead of rejecting them with ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION #13095. Such a peer is matched against the semver range carried by the specifier (work:5.x.x is checked as 5.x.x, npm:bar@^5 as ^5), or against * when it carries no version, while the original specifier still selects the package to auto-install. Bare name@version values, which are almost always a mistake, are still rejected.
Added pnpm doctor, which diagnoses the pnpm installation and the environment it runs in: the versions and install method, whether the global bin directory is on PATH, whether the store and cache are writable, which link strategies (reflink, hardlink, symlink) the store's filesystem supports, registry connectivity, and an offline file: install that exercises the resolve/store/link path end to end. Each check reports how to fix what it finds, and the command exits non-zero when any check fails.
Use --offline to skip the checks that need network access, --json for machine-readable output, and --benchmark to time the filesystem and install checks.
Added support for executing multiple scripts matching a RegExp passed to pnpm run (e.g., pnpm run "/^build:.*/"), running matched scripts in deterministic lexicographical order. Restored the --sequential (-s) CLI option for pnpm run, which forces workspaceConcurrency to 1 so that matched scripts run sequentially one by one across and within packages.
Fixed pnpm install failing with ERR_PNPM_LOCKFILE_IS_SYMLINK when pnpm-lock.yaml is a symlink, as build sandboxes such as Bazel and Nix stage it #13073. Reading a lockfile through a symlink is allowed again, and an install that leaves the lockfile unchanged no longer rewrites it, so --frozen-lockfile no longer needs to write at all. Writing a changed lockfile through a symlink is still refused, as that would redirect the write onto the symlink's target.
Fixed frozen installs incorrectly treating equivalent Git dependency specifiers as a stale lockfile. See #13039.
pnpm owner ls now reports authentication and authorization failures (401/403) as dedicated errors that include the registry's response body, matching pnpm owner add/rm, instead of a generic Failed to fetch owners message.
Recover from a metadata cache entry that disappears (concurrent cache cleanup, antivirus) after the registry has already answered the conditional request with 304 Not Modified. The metadata is re-requested once without cache validators instead of failing the install with ERR_PNPM_CACHE_MISSING_AFTER_304.
A project pinned to a broken pnpm release via packageManager or devEngines.packageManager now reports which release is broken and what to do about it, instead of failing inside the installer. pnpm self-update already refused these releases; the version switch does too.
Prevent broken-lockfile errors from including snippets of the lockfile's contents.
pnpm self-update now checks that the version it installed can run before making it the active pnpm. A release that installs but cannot execute is discarded with an error instead of replacing a working installation.
Fixed an out-of-memory regression when workspace projects concurrently resolve a package with large registry metadata pnpm/pnpm#13077.
Fixed pnpm update rewriting exact version pins that use the = operator (for example =3.5.1) to a caret range (^3.5.1). Exact pins are now preserved and written back as the bare version. See pnpm/pnpm#12745.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
Fixed pnpm pack applying workspace-root ignore rules when a workspace package has its own .npmignore file.
pnpm pack applying workspace-root ignore rules when a workspace package has its own .npmignore file.minimumReleaseAge approval prompt visible during pnpm install. The progress reporter now pauses its redraws while a prompt is waiting for input instead of overwriting it, so the install no longer hangs on a question the user cannot see #13019.pnpm self-update failing to link native platform binaries stored in sibling global virtual store slots.Added versioning.epics to pnpm-workspace.yaml. An epic ties a group of member packages to a lead package, constraining every member's major version to
Added versioning.epics to pnpm-workspace.yaml. An epic ties a group of member packages to a lead package, constraining every member's major version to a band derived from the lead's major: while the lead is on major M, members live in M*100 … M*100+99. Members move independently inside the band (patch, minor, and a major intent that stays in-band); a bump that would carry a member past the band ceiling is rejected until the lead advances its own major. When a release plan takes the lead to a new stable major, every member re-bases to the band floor in the same plan. Membership is matched with pnpm's package selectors — name globs, ./-prefixed directory globs, and !-prefixed negations.
Added the team command for managing organization teams and team memberships on the registry, with create, destroy, add, rm, and ls subcommands and support for --otp, --parseable, and --json flags.
Added native workspace release management #12952: the new pnpm change command records change intents as changesets-compatible .changeset/*.md files (pnpm change status shows the pending release plan), and the bare pnpm version -r consumes them — bumping versions across the workspace with dependent propagation through workspace: ranges, fixed groups, a maxBump cap, --filter narrowing, and --dry-run — writing changelogs, and recording consumed intents in a committed ledger that keeps cherry-picks and merge-backs between release branches safe. Packages can be moved onto per-package release lanes with the new pnpm lane <name> --filter <pkg> command and back with pnpm lane main --filter <pkg> (pnpm lane shows the membership), releasing X.Y.Z-lane.N prereleases from the same runs that release stable versions of the packages on the main lane. Configuration lives under the new versioning key of pnpm-workspace.yaml (fixed, ignore, maxBump, lanes, changelog). When two workspace projects publish the same name, intent files, versioning.lanes, and versioning.fixed/ignore may reference a project by its workspace-relative directory path (e.g. "./pnpm/npm/pnpm") — the one additive extension to the changesets format, applied automatically by pnpm change.
Release changelogs default to registry storage (versioning.changelog.storage): no CHANGELOG.md is committed. Each release's section is composed at publish time and packed into the published tarball on top of the previously published version's changelog, and the consumed change intents are garbage-collected by a later pnpm version -r only once the registry confirms the version is published with its section. Set versioning.changelog.storage: repository to keep committed CHANGELOG.md files instead.
Added a new override selector form with an empty range — "pkg@": "<version>" — called a convergence override. It rewrites a dependency edge only when its exact version satisfies the edge's declared range, so compatible consumers converge on one version while incompatible consumers keep their own resolution — now and for any dependent added in the future #12794.
overrides:
"form-data@": 4.0.6
The value must be an exact version. When a full resolution detects that every declared range also admits a newer version, pnpm warns that the override is stale and names the version to converge on. Previously an empty range in an override selector was undocumented and behaved like a bare (unscoped) override.
A tokenHelper set in the global pnpm auth.ini is no longer rejected as project-level configuration. The guard that blocks tokenHelper from a project .npmrc only treated ~/.npmrc as a trusted source, so a helper written to auth.ini (for example by pnpm config set) failed on every command and could not even be removed with pnpm config delete. A tokenHelper in a workspace or project .npmrc is still rejected.
pnpm cache delete now removes a package's metadata from every metadata cache directory (metadata, metadata-full, and metadata-full-filtered), instead of only the one the current resolution mode reads. Previously a package cached under a different mode (e.g. metadata-full-filtered) was left behind. Closes pnpm/pnpm#12753.
Fixed an injected workspace dependency (injectWorkspacePackages: true) incorrectly staying as file: instead of deduping back to link: when an unrelated, ordinary shared dependency resolved to a peer-suffixed variant for the target project's own copy but not for the injected occurrence. See pnpm/pnpm#10433.
pnpm deploy now supports workspaces that use catalogs.
Fixed pnpm deploy with a shared lockfile so local file: tarball dependencies keep their package name in the generated deploy lockfile. This prevents warm-store deploys from failing with ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE when the tarball filename includes the version.
Options that follow create, exec, or test appearing as a subcommand of another command are now parsed instead of being silently treated as positional parameters. For example, pnpm team create @org:team --registry <url> previously ignored the --registry option and sent the request to the default registry.
pnpm add -g, pnpm update -g, pnpm setup, and the self-updater no longer fail with ERR_PNPM_MISSING_TIME when trustPolicy: no-downgrade or resolutionMode: time-based is set in the global config #12883. The decision to fetch full registry metadata now lives in one place, and the no-downgrade trust policy always requests full metadata (matching the self-updater), since the trust evidence it checks is missing from abbreviated metadata even on registries that include the time field.
pnpm list and pnpm why no longer crash with EMFILE: too many open files when a project has a large number of unsaved dependencies (packages present in node_modules but not in the lockfile). The reads of those packages are now concurrency-limited.
The published pnpm package no longer declares dependencies or devDependencies. Because the CLI bundles its runtime dependencies into dist/node_modules, those fields are dropped when packing, so npm install of the tarball no longer tries to resolve internal-only packages such as @pnpm/test-ipc-server. Closes pnpm/pnpm#12955.
Fixed pnpm publish --otp and pnpm publish --batch --otp to send the configured OTP to the registry.
pnpm publish again sends the package's README to the registry as metadata, so registries can render it on the package page. The readme is always included in the published metadata (matching the npm CLI), while the embed-readme setting continues to control only whether the readme is written into the package.json inside the tarball. This restores the behavior that was lost when publishing became fully native. Closes pnpm/pnpm#12966.
Fixed the dependency status check wrongly reporting "up to date" when a package.json, .pnpmfile.cjs, or patch file was edited in the same second as the previous install, on filesystems that record mtimes at whole-second resolution (for example ext4 with 128-byte inodes). The optimistic repeat-install fast path and verify-deps-before-run compared mtimes strictly, so a same-second edit whose mtime rounded down looked unchanged and re-resolution was skipped. Such a file's whole second is now treated as possibly-modified, falling through to the content check; behavior on sub-second filesystems is unchanged.
Retry package metadata requests when a registry or proxy returns 304 Not Modified to an unconditional request, preventing false ERR_PNPM_CACHE_MISSING_AFTER_304 failures pnpm/pnpm#12882.
If the retry also returns 304, report ERR_PNPM_META_NOT_MODIFIED_WITHOUT_CACHE instead.
Fixed pnpm update removing transitive lockfile entries when dedupePeerDependents is disabled and the selected package is absent pnpm/pnpm#12456.
Limit modern deploy lockfiles and localized virtual stores to dependencies reachable from the selected dependency groups.
A tokenHelper command is now given a 60-second time limit. A helper that hangs (deadlock, stuck I/O) is killed and reported as an error instead of leaving the command waiting forever.
Fixed orphaned child processes on Windows when pnpm exits on an error while commands spawned by pnpm exec or pnpm dlx are still running (for example, when one project's command fails during pnpm --recursive exec). The PIDs of these commands are now recorded when they are spawned and their whole process trees are terminated with taskkill on an error exit. Previously the cleanup relied on enumerating the system process list, which is so slow on Windows that the enumeration hit its timeout and the cleanup was silently skipped #12406.
pnpm pack now respects workspace-root .npmignore and .gitignore files when packing workspace packages.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
a897ef7: Custom fetchers exported from a pnpmfile can now delegate by returning a { delegate: } envelope: pnpm rewrites the package's resolution to th
{ delegate: <resolution> } envelope: pnpm rewrites the package's resolution to the delegated shape and runs the built-in fetcher on it. This is the portable delegation form that also works in pacquet, where cafs and fetchers cannot be passed to the hook. Related to pnpm/pnpm#11685.2b02764: The changed-packages filter (--filter "...[<since>]") no longer allows an option-like <since> value (such as --output=<path>) to be interpreted as a git option — git now rejects it as a bad revision. The repository root is also resolved to the nearest .git entry, so the filter works in a git worktree checked out inside another repository's tree.
43711ce: pnpm outdated no longer checks the registry for dependencies that are resolved from local link:, file:, or workspace: references in the lockfile #12827.
3c6718b: Fixed a deadlock in peer dependency resolution: pnpm install hung forever when a peer dependency cycle spanned a project's own dependencies and auto-installed peer providers, for example when installing electron-builder@26.15.3 #12921.
252f15e: Fixed peer dependency auto-install picking a version the peer range rejects. In a workspace with several projects, a package declaring a peer dependency with a semver range (for example ^1.0.0) could get the highest version found anywhere in the workspace (for example a 2.0.0 resolved for another project) instead of a version that satisfies the range. Peers are now deduplicated onto the highest preferred version that satisfies the declared range, and when none does, the range is resolved from the registry.
Also fixed re-resolving with an existing lockfile hoisting a different peer version than a fresh install of the same manifest: root dependencies reused from the lockfile were invisible to peer hoisting, so a peer that a root dependency provides could be bound to another version.
a38adda: pnpm self-update <version> now installs the requested pnpm version when it matches the currently running version but is missing from the global self-update directory.
6a85968: pnpm stage list now stops paginating after a fail-safe cap of 1000 pages, so a misbehaving registry cannot keep the command looping forever.
eee7c9a: verify-deps-before-run no longer spawns a pnpm install when pnpm is executed in a directory that has no package.json. A mistyped command run outside a project (for example pnpm witch 10 login) used to crash with a confusing error from the spawned install; now it fails with the regular "no package.json found" error.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
51300fd: Fixed a path traversal vulnerability where a dependency whose manifest name was a scoped path traversal (e.g. @x/../../../ ) could be written…
pnpm access command for managing package access and visibility on the registry, supporting listing packages and collaborators, getting and setting package status and MFA requirements, and granting or revoking team access.allowBuilds entries for git-hosted packages to match by repository URL without pinning the resolved commit hash. This lets trusted git repositories keep running their build scripts after branch updates without approving each new commit, while package-name-only rules still do not approve git-hosted artifacts.minimumReleaseAge or trustPolicy enabled) this cuts peak RSS by roughly 30%, back in line with pnpm 10. The resolved lockfile is unchanged.pnpm-lock.yaml from writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g. ../../../tmp/x@1.0.0) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshot version: "../../x") is now rejected at formatGlobalVirtualStorePath, the single point every global-virtual-store slot path funnels through — closing the same escape in the isolated linker, the resolver's dependency-graph builder, and the config-dependency installer.pnpm-lock.yaml files when reading or writing the env lockfile document.registries and namedRegistries to be configured in the global config.yaml file.name was a scoped path traversal (e.g. @x/../../../<path>) could be written outside node_modules to an attacker-controlled location during pnpm install, even with --ignore-scripts. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.pnpm-lock.yaml when the registry cannot resolve it. Previously, when registry metadata lacked a version that the lockfile already pinned (for example, a mirror that had not synced a recent release yet), pnpm install and pnpm dedupe silently dropped the optional dependency's entries — emptying maps such as the platform binaries of @napi-rs/canvas — so the lockfile differed between machines and frozen installs on other hosts had nothing to link #12853.autoInstallPeers when a workspace package depends on a version of a package that a transitive dependency's self-contained closure also provides for itself. The peer providers that are attached to the root project for reuse are no longer peer-resolved a second time in the root context, so packages inside such a closure no longer get their peers bound to the root project's incompatible version #4993.${...} environment-variable placeholders in the httpProxy, httpsProxy, noProxy, proxy, and noproxy settings are no longer expanded when these settings come from a project's pnpm-workspace.yaml. They now receive the same protection already applied to registry, namedRegistries, and pnprServer.pnpm publish no longer prints credentials when the target registry is configured with inline user:pass@ credentials (e.g. registry=https://user:pass@example.com/). They are now redacted both from the "publishing to registry" line and from the OIDC (trusted publishing) failure messages.pnpm self-update now honors trustPolicy=no-downgrade. It resolves the target pnpm version against full registry metadata, so it refuses to switch to a version whose supply-chain trust evidence is weaker than an earlier-published one, the same way a regular install does.pn alias in generated shell completion scripts.pnpm runtime set <name> <version> now validates its arguments: the name must be node, deno, or bun, and the version must not contain a comma. Previously these were interpolated straight into a pnpm add selector, where an unsupported name or a comma (e.g. node 22,is-positive) could be misread as a list of packages or a local directory and install unintended packages or bins.<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
The install layer now always seeds preferredVersions from the lockfile, and caller-supplied preferred versions (such as the vulnerability penalties of…
e2e3c81: Added the issues command as an alias of bugs, so pnpm issues opens the package's bug tracker URL in the browser.
8491f8e: Added the prefix command which prints the current package prefix directory (or global prefix directory if -g / --global is used).
3425e80: Added an _auth setting for configuring registry authentication as a single structured (URL-keyed) value. It can be set in the global pnpm config (config.yaml) or, for CI, via the pnpm_config__auth environment variable. The env form sidesteps the GitHub Actions / bash / zsh limitation that broke the existing pnpm_config_//host/:_authToken=… form (env var names containing /, :, or . are silently dropped). Closes pnpm/pnpm#12314.
The value is keyed by registry URL so each secret is explicitly bound to the host that may receive it. Registry URL keys must use http or https and must not include credentials, query strings, or fragments:
export pnpm_config__auth='{"https://registry.npmjs.org":{"@":{"authToken":"npm-token"},"@org":{"authToken":"org-token"}}}'
The equivalent in the global config.yaml:
_auth:
https://registry.npmjs.org:
"@":
authToken: npm-token
"@org":
authToken: org-token
Within each registry URL, @ means registry-wide/default credentials and package scopes like @org bind credentials to that scope on the same host. The only supported credential field is authToken (maps to _authToken / bearer auth); the deprecated basicAuth / username + password forms are intentionally not accepted here.
Each entry also infers a trusted registry route: @ routes the default registry (and pnpm add <pkg> resolves there), and @org routes that scope. Because the credential and destination host arrive in one trusted value, repo-controlled pnpm-workspace.yaml or project .npmrc cannot redirect the token to a different host. _auth is honored only from the env var and the global config — it is ignored in a project pnpm-workspace.yaml / .npmrc, so repo-controlled config can never supply registry auth. Precedence: CLI flags (--registry, --@scope:registry) > pnpm_config__auth > global config.yaml _auth > pnpm-workspace.yaml.
Both pnpm_config__auth (lowercase, documented form) and PNPM_CONFIG__AUTH (all-caps, the shell convention some CI runners apply) are honored. If both are set, lowercase wins unless it is empty, in which case uppercase is used. The env var wins over the global config.yaml _auth on a conflicting key. tokenHelper is not supported in _auth. Parsing is strict: a malformed value (bad JSON, wrong shape, invalid registry URL or scope, an unsupported credential field) fails fast with an error rather than being silently dropped.
Pacquet parity note: the pacquet (Rust) port supports the same single credential field as the TS CLI: authToken.
a33eeec: pnpm self-update and packageManager version-switching can now install and link pnpm v12 (the Rust port), published with equal content under both the pnpm and @pnpm/exe names on the next-12 dist-tag. Its native binaries ship as @pnpm/exe.<platform>-<arch> packages, which pnpm's built-in installer links directly — no Node.js launcher, so the command pays no Node startup cost. v12 is initialized exactly like @pnpm/exe, including per-platform global-virtual-store hashing. From v12 onward the install converges on the unscoped pnpm package (the Rust exe) — even when updating from the SEA @pnpm/exe build.
1dd12bd: When resolving through a pnpr install-accelerator server, pnpm no longer forwards its own upstream registry credentials in the resolve request. Only the Authorization header identifying the caller to pnpr is sent. The pnpr server now selects upstream credentials from its own route policy (operator-configured upstream credential aliases), so private dependencies resolve through a pnpr-managed alias the caller is authorized to use, rather than by sending the client's registry tokens to the server.
1e81761: Expose web authentication authUrl and doneUrl in JSON error output when OTP is required in a non-interactive terminal #12724.
2f389d6: Added the Node.js release team's new signing key (Stewart X Addison, 655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD) to the embedded Node.js release keys, so runtimes whose SHASUMS256.txt is signed by the new releaser verify successfully.
acbdb94: Fixed shell tab completion not suggesting workspaces after the -F alias for --filter option.
dcabb78: Fixed pnpm up -r <pkg> bumping unrelated packages that have open semver ranges. Previously, any update mutation nullified the lockfile-derived preferredVersions globally, so packages with ^x.y.z ranges could re-resolve to newer compatible versions even though the user only asked to update a specific package. The install layer now always seeds preferredVersions from the lockfile, and caller-supplied preferred versions (such as the vulnerability penalties of pnpm audit --fix) layer on top of the seed instead of replacing it. The targeted package still bumps: the per-resolve updateRequested flag makes the resolver ignore the target's own lockfile pins.
Closes pnpm/pnpm#10662.
d539172: Fixed pnpm pack and pnpm publish failing when prepack generates files that are included in the package and postpack cleans them up.
be6505a: Hardened global package management:
node.exe flavor of a bin, so a stale node.exe no longer survives on PATH after uninstall, and a new global install no longer silently overwrites an existing node.exe.pnpm add -g pnpm@<version> (and @pnpm/exe@<version>) is now rejected like the bare pnpm form, pointing to pnpm self-update.node_modules paths, preventing a tampered manifest from escaping the install directory.25c7388: pnpm now rejects jsr: specifiers whose package name is not a valid npm package name — an empty scope or name (e.g. jsr:@scope/), path separators inside the name, or any other shape validate-npm-package-name rejects — with ERR_PNPM_INVALID_JSR_PACKAGE_NAME instead of silently converting them into a malformed @jsr/... npm package name.
25c7388: pnpm now rejects named-registry specifiers (e.g. gh:) whose package name is not a valid npm package name — an empty scope (e.g. gh:@/bar), path separators inside the name (e.g. gh:@scope/../name), or any other shape validate-npm-package-name rejects — with ERR_PNPM_INVALID_NAMED_REGISTRY_PACKAGE_NAME instead of passing the name through to registry URLs and metadata cache file paths.
96da7c5: node-gyp's gyp_main.py and gyp entrypoints are now packed with the executable bit in the pnpm and @pnpm/exe tarballs. Without it, building native addons from source could fail with a permission error.
99982b9: Sped up resolution and reduced memory use against registries that ignore npm's abbreviated metadata format and always return the full package document (for example, Azure DevOps Artifacts). pnpm now strips such documents down to the abbreviated field set before caching them. Resolution output is unchanged, and registries that honor the abbreviated format (such as the npm registry) pay no extra cost.
11a7fdd: Sped up offline and --prefer-offline resolution on large workspaces (e.g. pnpm dedupe --offline, pnpm install --offline). Package metadata loaded from the local cache is now kept in memory, so each package's metadata is parsed once per command instead of once per dependent that references it.
2c7369d: pnpm pack-app now rejects --entry / pnpm.app.entry and --output-dir / pnpm.app.outputDir values that are absolute paths or escape the project directory via .. (or a symlink that resolves outside it), and refuses to write the produced executable when its target path already exists as a symlink (or other non-regular file). This prevents a repository-controlled package.json from embedding host files (such as an SSH key) into the produced executable, writing build artifacts outside the project, or overwriting an arbitrary file through a committed symlink. The new error codes are ERR_PNPM_PACK_APP_ENTRY_OUTSIDE_PROJECT, ERR_PNPM_PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT, and ERR_PNPM_PACK_APP_OUTPUT_FILE_NOT_REGULAR.
When ad-hoc signing macOS targets, pnpm pack-app now runs the system codesign by absolute path and resolves ldid to a location outside the project, so a repository-controlled node_modules/.bin on PATH cannot hijack the signer.
ce5d5a5: Relative paths in patchedDependencies are now resolved against the lockfile directory when computing patch file hashes, so running pnpm install from a subdirectory no longer fails with ENOENT looking for the patch file in the wrong location #12762.
ebb4096: pnpm peers no longer reports a conflict for a missing peer dependency that is ignored via pnpm.peerDependencyRules.ignoreMissing.
dcabb78: Fixed a prototype-pollution hazard when seeding preferred versions: a dependency named __proto__ in a manifest or in pnpm-lock.yaml could write through Object.prototype (or crash the install) while the preferred-versions map was being built. The maps are now null-prototype objects, so crafted package names land as plain keys.
f38e696: Hardened pnpm deploy --force so it refuses unsafe deploy targets such as workspace roots, parent directories, out-of-workspace paths, and symlinked target parents.
806c3ec: pnpm no longer warns about ignored project-level auth settings when PNPM_CONFIG_NPMRC_AUTH_FILE points at the project .npmrc — setting it to that file is an explicit opt-in to trusting it, so auth env variables in it are expanded pnpm/pnpm#12480.
991405e: Restore differential rendering (ansi-diff) to fix duplicated output lines introduced by pnpm/pnpm#12351.
c121235: Fixed the topological order of --filtered commands (pnpm run, pnpm exec, pnpm publish, pnpm pack, pnpm rebuild) when the selected projects depend on each other only transitively through projects that were not selected. Previously such selected projects could run concurrently or in the wrong order; now a project always runs after the selected projects it transitively depends on, while projects without a real dependency relationship still run concurrently. This now also holds for prod-only filters (--filter-prod), which resolve order through the production dependency graph so transitive production dependencies are respected without pulling back the dev dependencies the filter drops, and for selections that mix --filter with --filter-prod #8335.
d539172: pnpm pack and pnpm publish no longer follow a symlinked workspace LICENSE file when injecting it into a package that has no license of its own. Following the symlink could pack bytes from outside the workspace into the published tarball.
dcabb78: Fixed pnpm up <pkg> producing a different result than a fresh install of the same manifests would. The resolver now distinguishes updateRequested (true only for packages that match the user's update target) from the broader update flag, and for the targeted package ignores only its own lockfile-derived preferred-version pins — so the target re-resolves exactly as if its lockfile entries were deleted and pnpm install ran. Preferred versions a fresh install applies (manifest pins, versions propagated down the dependency chain, and the vulnerability-avoidance penalties of pnpm audit --fix) stay in effect, so an update never installs duplicate versions that a reinstall from scratch would not reproduce. When a preferred version holds the update target below the newest version its range admits, pnpm now prints a warning explaining that reaching the newer version everywhere requires an override.
dcabb78: pnpm update <dep>@<version> now prints a warning when <dep> is only present as a transitive dependency: the requested version cannot be applied there (updates resolve the target the way a fresh install would), and the warning recommends adding the version to pnpm.overrides instead, which is the mechanism that does pin transitive dependencies. Closes pnpm/pnpm#12744.
a6c4d5f: When a dependency cannot be found in the registry (404) or the registry has no matching version, and a workspace project with the same name exists only at non-matching versions, the error now reports the available workspace versions (ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE) instead of the raw registry failure pnpm/pnpm#1379. Other registry failures (authorization, network, server errors) still propagate unchanged. The pacquet (Rust) resolver applies the same behavior.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
d3f68e2: Fix a pnpm audit performance regression on lockfiles that contain dependency cycles. The reachable-vulnerability pruning added in pnpm 11.5.1…
--lockfile-only: the tarball is downloaded so its integrity can be computed. A lockfile entry that is still missing its integrity is rejected as a ERR_PNPM_MISSING_TARBALL_INTEGRITY lockfile verification violation (the install fails closed) rather than being silently re-fetched.--exclude-peers to pnpm sbom. With auto-install-peers (the default), peer dependencies resolve into the lockfile and are otherwise indistinguishable from the package's own dependencies. The flag drops peer dependencies (and any transitive subtree reachable only through them) from the SBOM. CycloneDX 1.7 has no scope or relationship that expresses "consumer-provided peer", so omission is the only spec-clean handling. The flag name matches pnpm list --exclude-peers; note the SBOM flag prunes a peer's exclusive subtree, which is stricter than pnpm list (which only hides leaf peers).25a829e: pnpm audit --fix now writes a single combined minimumReleaseAgeExclude entry per package (e.g. axios@0.18.1 || 0.21.1) instead of one entry per version, matching the format documented for the setting. Existing per-version entries in pnpm-workspace.yaml are merged into the combined form rather than left as duplicates. Installs that auto-collect immature versions into minimumReleaseAgeExclude now report the same combined entries, so the "Added N entries" message matches what is written to the manifest #12534.
1cbb5f2: Fixed non-deterministic peer resolution that could add or remove an optional transitive peer — for example @babel/core, reached through styled-jsx — from a package's peer-dependency suffix across otherwise identical installs, churning the lockfile and causing intermittent pnpm dedupe --check failures in CI. When a package's children are resolved by one occurrence (the "owner") and reused by a deeper consumer, whether that consumer inherited the owner's missing peers depended on whether the owner's resolution had finished yet — a race under concurrent resolution. The decision is now a function of the dependency graph's structure rather than resolution-completion order.
d577eea: Fixed a Windows flakiness in pnpm dlx where a failed install could surface a spurious EBUSY: resource busy or locked error. The cleanup of a partially-populated dlx cache is now best-effort with retries and no longer masks the original error.
ec7cf70: Shortened the pnpm dlx cache path so deep dependency trees no longer overflow Windows' MAX_PATH, which could make a dependency's lifecycle script fail with spawn cmd.exe ENOENT.
05b95ab: Fixed pnpm hanging (and crashing with an unhandled promise rejection) when a non-retryable network error such as SELF_SIGNED_CERT_IN_CHAIN occurs while fetching from a registry. The error is now rejected through the returned promise instead of being thrown inside the detached retry callback.
d3f68e2: Fix a pnpm audit performance regression on lockfiles that contain dependency cycles. The reachable-vulnerability pruning added in pnpm 11.5.1 only memoized acyclic subtrees, so any node whose subtree touched a cycle — together with all of its ancestors — was recomputed on every query, making the path walk quadratic. Reachability is now computed once per node using Tarjan's strongly-connected-components algorithm, so cyclic graphs are handled in linear time #12212.
The audit path walk also no longer recurses, so a deeply nested dependency graph can no longer overflow the call stack, and the install path to each finding is tracked without per-node copying, keeping memory linear in the graph depth.
322f88f: Fix failed optional dependency updates so they don't rewrite unrelated dependency specs #11267.
1488db1: When enableGlobalVirtualStore is toggled on for a project that was previously installed without it, stale hoisted symlinks under node_modules/.pnpm/node_modules are now replaced instead of being left pointing at the old per-project virtual store location #9739.
6545793: Fixed pnpm install --ignore-workspace overwriting the allowBuilds map in pnpm-workspace.yaml. The ignored builds of a package with a build script were auto-populated into allowBuilds even though --ignore-workspace was passed, clobbering committed true/false values with the set this to true or false placeholder #12469.
fbdc0eb: Fixed minimumReleaseAgeExclude and trustPolicyExclude so multiple exact-version entries for the same package behave the same as a single || disjunction entry. Previously only the first matching rule's versions were honored, so a config like [form-data@4.0.6, form-data@2.5.6] could still flag form-data@2.5.6 as violating minimumReleaseAge, while [form-data@4.0.6 || 2.5.6] worked as expected #12463.
fa7004b: The in-memory package metadata cache is now populated on the exact-version disk fast path, so repeated resolutions of the same package within one install no longer re-read and re-parse the on-disk metadata. In large monorepos this brings the time for adding a new package down from minutes to seconds. The in-memory cache key now also includes the registry, so a package of the same name served by two different registries in a single install can no longer share a cache slot and resolve the wrong tarball.
0a154b1: Fixed pnpm patch dropping the package name (and leaking internal option fields) when the patched dependency resolves to a single git-hosted version.
4d3fe4b: The pnpr resolver endpoints moved under the reserved /-/pnpr namespace: POST /v1/resolve is now POST /-/pnpr/v0/resolve and POST /v1/verify-lockfile is now POST /-/pnpr/v0/verify-lockfile. The capability handshake at GET /-/pnpr advertises protocol version 0 to match. This keeps every pnpr-proprietary route in npm's reserved namespace, so it can never collide with a package path.
0ec878d: Removing a runtime dependency now removes the matching devEngines.runtime or engines.runtime entry that was materialized from it. Blank runtime selectors are normalized to latest.
17e7f2c: pnpm sbom now emits a CycloneDX issue-tracker external reference for components (and the root) whose package.json declares a bugs URL. Email-only bugs entries are skipped, since the reference requires a URL.
a84d2a1: Add @pnpm/resolving.tarball-url, which builds and recognizes the canonical npm tarball URL of a package. It vendors getNpmTarballUrl (previously the external get-npm-tarball-url package) and adds isCanonicalRegistryTarballUrl, the predicate the lockfile writer uses to decide whether a tarball URL is derivable from name+version+registry (and can therefore be omitted from pnpm-lock.yaml).
Exposing isCanonicalRegistryTarballUrl lets a custom resolver (pnpmfile resolvers) fronting a proxy that serves tarballs on a non-canonical path (e.g. an ephemeral localhost:<port>) rewrite the resolved tarball to the canonical form, so nothing host-specific is persisted to the lockfile. Previously this logic was private to @pnpm/lockfile.utils.
Two correctness fixes are included while consolidating the logic: the scoped-package unescape now handles uppercase %2F as well as %2f (percent-encoding is case-insensitive), and protocol-insensitive comparison strips only a leading http(s):// scheme instead of splitting on the first :// (which could truncate URLs containing a later ://).
852d537: Lockfile verification no longer reports a registry metadata fetch failure (for example a 403/401 on a private registry, or a network error) as ERR_PNPM_TARBALL_URL_MISMATCH. When the registry can't be reached to verify an entry, the install now aborts with the registry's own fetch error (such as ERR_PNPM_FETCH_403, which already explains the authentication situation) instead of mislabeling a transport failure as lockfile tampering. Registry fetch errors no longer leak basic-auth credentials embedded in the registry URL (https://user:pass@host/) into their message.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
c112b61: Added a --dry-run option to pnpm install. It runs a full dependency resolution and reports what an install would change, but writes nothing t
c112b61: Added a --dry-run option to pnpm install. It runs a full dependency resolution and reports what an install would change, but writes nothing to disk (no lockfile, no node_modules) and always exits with code 0. This mirrors the preview semantics of npm install --dry-run #7340.
179ebc4: pnpm run --no-bail now exits with a non-zero exit code when any of the executed scripts fail, while still running every matched script to completion. This makes the exit-code behavior of --no-bail consistent between recursive and non-recursive runs (recursive runs already failed at the end). Previously, a non-recursive pnpm run --no-bail always exited with code 0, even when a script failed #8013.
0474a9c: Added support for generating Node.js package maps at node_modules/.package-map.json during isolated and hoisted installs. Added the node-experimental-package-map setting to inject the generated map into pnpm-managed Node.js script environments, and the node-package-map-type setting to choose between standard and loose package maps.
dcededc: pnpm sbom now marks components reachable only through devDependencies with CycloneDX scope: "excluded" and the cdx:npm:package:development property. The excluded scope documents "component usage for test and other non-runtime purposes", which matches the semantics of a devDependency; the property is the CycloneDX npm-taxonomy marker emitted by @cyclonedx/cyclonedx-npm, so both modern (scope) and existing (property) consumers are covered. Components reachable at runtime (including installed optionalDependencies) omit scope and default to required.
1495cb0: Added per-package SBOM generation with --out and --split flags. Use --out out/%s.cdx.json to write one SBOM per workspace package to individual files, or --split for NDJSON output to stdout. When --filter selects a single package, the SBOM root component now uses that package's metadata. Workspace inter-dependencies (workspace: protocol) and their transitive dependencies are included. Author, repository, and license fall back to the root manifest when the package doesn't define them.
293921a: feat(view): support searching project manifest upward when package name is omitted
When running pnpm view without a package name, the command now searches
upward for the nearest project manifest (package.json, package.yaml, or package.json5) and uses its name field.
If the manifest exists but lacks a name field, an error is thrown.
This change also replaces the find-up dependency with empathic for
improved performance and consistency across workspace tools.
29ab905: Fixed pnpm update overriding the version range policy of a named catalog whose name parses as a version (e.g. catalog:express4-21). The catalog: reference carries no pinning of its own, so the prefix from the catalog entry (such as ~) is now preserved instead of being widened to ^ #10321.
bee4bf4: Security: validate config dependency names and versions from the env lockfile (pnpm-lock.yaml) before using them to build filesystem paths. A committed lockfile with a traversal-shaped configDependencies name (such as ../../PWNED) or version (such as ../../../PWNED) could previously cause pnpm install to create symlinks or write package files outside node_modules/.pnpm-config and the store. Names must now be valid npm package names and versions must be exact semver versions; the same validation is applied to optional subdependencies of config dependencies, and to the legacy workspace-manifest format before any lockfile is written. See GHSA-qrv3-253h-g69c.
96bdd57: Fix link: workspace protocol switching to file: after pnpm rm is run from inside a workspace package whose target workspace dependency has its own dependencies, when injectWorkspacePackages: true is set. Follow-up to #10575, which fixed the same symptom for workspace packages without dependencies.
302a2f7: No longer warn about using both packageManager and devEngines.packageManager when the two fields pin the same package manager at the same version with the same integrity hash (e.g. both pnpm@11.5.1+sha512.…). Previously the hash was stripped from the legacy packageManager field but not from devEngines.packageManager, so even identical specifications looked like a mismatch #12028.
The warning still fires on any genuine divergence, and several cases now state the specific reason instead of a single generic message: a different package manager, a different version, or contradictory integrity hashes for the same version.
3f0fb21: Fixed the progress line showing leftover characters from external processes that write to the terminal between progress updates (e.g. an SSH passphrase prompt would leave a fragment like added 0sa':). The interactive reporter now redraws each frame in place, erasing to the end of the display before reprinting, so any such remnants are cleared #12350.
564619f: Fixed pnpm approve-builds reporting "no packages awaiting approval" when a build-script dependency whose approval was revoked (e.g. after git stash drops the allowBuilds from pnpm-workspace.yaml) is re-added. The revoked packages are now correctly recorded in .modules.yaml so approve-builds can find them. #12221
3d1fd20: Skip the redundant "target bin directory already contains an exe called node" warning on Windows when the existing node.exe already matches the target (same hard link or identical content) pnpm/pnpm#12203.
1b02b47: Fix macOS Gatekeeper blocking native binaries (.node, .dylib, .so) by removing the com.apple.quarantine extended attribute after importing them from the store.
When pnpm imports files from its content-addressable store into node_modules, macOS preserves extended attributes, including com.apple.quarantine. If this xattr is present on a store blob (e.g. it was first written under a Gatekeeper-enabled app such as a Git client), it propagates to node_modules, and Gatekeeper blocks the native binary from loading even though pnpm already verified the file's integrity against the lockfile.
After importing a package, pnpm now strips com.apple.quarantine from its native binaries, matching Homebrew's behaviour of dropping quarantine from verified downloads. The cleanup is macOS-only, runs in a single batched xattr call per package, is restricted to native binaries (other files are untouched), and is non-fatal (it logs a warning on unexpected errors).
Fixes #11056
61969fb: Fix pnpm install with optimisticRepeatInstall incorrectly reporting Already up to date when pnpm-lock.yaml changed but project manifests did not. This affected workflows such as checking out or restoring only the lockfile #12100.
Also fixes checkDepsStatus to use the correct lockfile path when useGitBranchLockfile is enabled, so the optimistic fast-path and lockfile modification detection work with pnpm-lock.<branch>.yaml files instead of always stat'ing pnpm-lock.yaml. Merge-conflict detection now reads the resolved lockfile name as well, and with mergeGitBranchLockfiles enabled every pnpm-lock.*.yaml is scanned for modifications and conflicts. The git branch is now resolved by reading .git/HEAD directly (no process spawn) and uses the workspace directory rather than process.cwd().
5c12968: Fix recursive updates of transitive dependencies when the update command mixes transitive dependency patterns with direct dependency selectors. For example, pnpm up -r "@babel/core" uuid now updates matching transitive @babel/core dependencies even when uuid is a direct dependency selector #12103.
9d79ba1: Register the pnpm update --no-save flag in the CLI help and option parser.
0474a9c: Fixed pnpm import for Yarn v2 lockfiles when js-yaml v4 is installed.
9e0c375: Fixed pnpm install repeatedly prompting to remove and reinstall node_modules in a workspace package when enableGlobalVirtualStore is enabled. The post-install build step recorded a per-project node_modules/.pnpm virtual store directory in node_modules/.modules.yaml, overwriting the global <storeDir>/links value the install step had written. The next install then detected a virtual-store mismatch (ERR_PNPM_UNEXPECTED_VIRTUAL_STORE). The build step now derives the same global virtual store directory as the install step #12307.
223d060: Document the --cpu, --os and --libc flags in the output of pnpm install --help. These flags were already supported but were only documented on the website #12359.
e85aea2: Avoid reading README.md from disk when publishing if the publish manifest already provides a readme field. The README is now only read lazily, inside createExportableManifest, when it is actually needed.
3188ae7: Fixed pnpm peers check to accept loose peer dependency ranges such as >=3.16.0 || >=4.0.0- when the installed peer version satisfies the range #12149.
531f2a3: Fixed pnpm update rewriting a workspace: dependency that points at a local path (e.g. workspace:../packages/foo/dist) into a normalized link: or version-range specifier. Such specifiers are now preserved verbatim when the workspace protocol is preserved #3902.
fe66535: Fixed a lockfile non-convergence bug where an incremental install kept a duplicate transitive dependency that a fresh install would not produce. When a package is reused from the lockfile, its child edges are taken verbatim and bypass the preferred-versions walk, so a transitive dependency could stay pinned to an older version even after a direct dependency resolved to a higher version that satisfies the same range. The resolver now refreshes such a stale pin to the higher direct-dependency version during resolution — so the older version is never resolved or fetched, and the incremental result converges to the fresh one.
6d35338: pnpm install detects changes inside local file dependencies again. The optimistic repeat-install fast path only tracks manifest and lockfile modification times, so edits inside a local dependency's directory (or a repacked local tarball) were reported as "Already up to date". Projects with local file dependencies (file: and bare local path or tarball specifiers, declared directly or through pnpm.overrides) now always run a full install, which refetches those dependencies, matching pnpm v10 behavior #11795.
4ca9247: Preserve the existing Node.js runtime version prefix when resolving node@runtime:<range> to a concrete version.
30c7590: Create shorter CAFS temporary package directories to leave room for lifecycle scripts that create IPC socket paths under TMPDIR.
13815ad: Reporter output (warnings, progress) for pnpm store and pnpm config subcommands now goes to stderr instead of stdout. This fixes scripts that capture their stdout (e.g. PNPM_STORE=$(pnpm store path), pnpm config list --json | jq) from getting warnings mixed into the result.
1c05876: Avoid relinking unchanged child dependencies and remove stale child links during warm installs.
817f99d: Fixed lockfile churn where a package's transitivePeerDependencies could be dropped (and shift between packages) when the package participates in a dependency cycle. A cycle re-entry resolves against truncated children, so it must not be cached as "pure"; otherwise sibling occurrences of the same package short-circuit and lose transitive peers depending on traversal order #5108.
eba03e0: Fix pnpm install reporting "Already up to date" after a catalog entry in pnpm-workspace.yaml was reverted to a previous version. After an update modified a catalog, the workspace state cache stored the pre-update catalog versions, so reverting the entry back to its original version was not detected as an outdated state #12418.
3b54d79: pnpm update now keeps lockfile overrides that resolve through a catalog in sync with the catalog. Previously, when an override referenced a catalog (e.g. overrides: { foo: 'catalog:' }) and pnpm update bumped that catalog entry, the lockfile's catalogs advanced while the resolved overrides kept the old version. The resulting lockfile was internally inconsistent, so a later pnpm install --frozen-lockfile failed with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.
9d0a300: Fixed pnpm version --recursive so it honors the workspace selection. In recursive mode the version bump now applies to the packages resolved from the workspace filter (selectedProjectsGraph), matching the behavior of pnpm publish --recursive, instead of always bumping every workspace package #11348.
<!-- sponsors -->
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" /> <img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" /> </picture> </a> </td> </tr> </tbody> </table>
<table> <tbody> <tr> <td align="center" valign="middle"> <a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" /> <img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" /> <img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" /> <img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" /> <img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" /> <img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" /> </picture> </a> </td> </tr> <tr> <td align="center" valign="middle"> <a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" /> <img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" /> </picture> </a> </td> <td align="center" valign="middle"> <a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"> <picture> <source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" /> <source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" /> <img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" /> </picture> </a> </td> </tr> </tbody> </table>
<!-- sponsors end -->
Your coding agent can read these notes before it upgrades. Set up the MCP server →