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
03 Oct 2026
Ships on a steady schedule
a new release about every 8 days
Nearly every release is documented
notes for 57 of the last 60 stable releases
376 versions withdrawn
withdrawn after publishing
14 years old
1338 releases · first in 2013
This release moves the WebContainer build into a separate @pnpm/wasm package, shrinks the pnpm package back to about 4 MB, and fixes pnpm publish with
This release moves the WebContainer build into a separate @pnpm/wasm package, shrinks the pnpm package back to about 4 MB, and fixes pnpm publish with provenance from GitLab CI.
The WebAssembly build for StackBlitz WebContainers now ships as a separate @pnpm/wasm package. The pnpm and @pnpm/exe packages no longer include it, which brings their unpacked size back from about 55 MB to about 4 MB. In a WebContainer, install @pnpm/wasm with npm to get the pnpm command.
pnpm publish with provenance from GitLab CI is no longer rejected by the npm registry with a 422 error. The provenance statement now includes the GitLab CI variables in invocation.parameters, as npm does #16551.
pnpm audit signatures now uses the TLS settings of the redirect target when a registry redirects its signing-keys request, for example to registry.npmjs.org. A cafile scoped to a private registry no longer makes the redirected request fail #16541.
Fixed pnpm install --frozen-lockfile rejecting an up-to-date lockfile when an injected workspace package uses a catalog entry in peerDependencies #16557.
The [<since>] filter selector works again with Git 2.24 through 2.27 #16561. With Git older than 2.24, the selector now fails with an error that names the required Git version.
It also detects changes in projects whose directory names contain non-ASCII characters. Such a change used to be credited to the parent project. changedFilesIgnorePattern and testPattern now match changed files whose names contain non-ASCII characters.
The pnpm executable is about 10% smaller. On macOS arm64 it went from 45.1 MB to 40.3 MB.
Sped up trust downgrade checks for packages with long release histories.
With optimisticRepeatInstall: false, pnpm install now runs the projects' own lifecycle scripts, such as prepare, even when node_modules is already up to date #16545.
pnpm self-update now fails for Homebrew-installed pnpm and prints the brew upgrade command for the installed formula, such as brew upgrade pnpm or brew upgrade pnpm@11. It used to install a second copy of pnpm that the Homebrew one kept shadowing #16547.
One column per quarter.
…project in the store. It also carries a security fix for pnpm login.
This release runs pnpm in StackBlitz WebContainers, adds a per-registry networkConcurrency setting, and records every installed project in the store. It also carries a security fix for pnpm login.
pnpm now automatically uses WebAssembly in StackBlitz WebContainers, including when installation scripts are disabled. Native installations continue to use the native executable when installation scripts are enabled.
A registries entry can now set networkConcurrency, the most requests pnpm keeps in flight to that registry's origin. Requests to other registries keep the overall limit. The setting may live in pnpm-workspace.yaml or the global config.yaml.
registries:
https://npm.corp.example.com/:
scopes: ["@acme"]
networkConcurrency: 4
pnpm install now records every project it installs in the store's projects directory, as a symlink to the project directory. A --frozen-store install without the global virtual store still records nothing. Only projects that used the global virtual store were recorded before #6929.
pnpm login no longer forwards credentials in its request body to another origin during redirects.Fixed pnpm install failing on Android with ERR_PNPM_STORE_DIR_ACQUIRE_OPERATION_LOCK #16508.
pnpm install --frozen-lockfile again succeeds when a workspace project recorded in pnpm-lock.yaml has no directory, such as a project left out of a Docker build context. It still fails if the project's directory exists without a package.json #16453.
pnpm install --frozen-lockfile no longer requires a pnpm-lock.yaml in a project that has no dependencies. It also succeeds when pnpm-lock.yaml records only the pinned pnpm version, as other commands write it when they run before the first install #16477.
Fixed pnpm install --frozen-lockfile rejecting a fresh lockfile when an injected workspace dependency has an optional peer supplied by another workspace project #16428.
With nodeLinker: hoisted, a filtered install now keeps the packages of the workspace projects an earlier install put in node_modules. This also covers the install that pnpm --filter <selector> run and pnpm --filter <selector> exec start before the command. Before, these installs removed every package that only the unselected projects needed #16483.
pnpm install with nodeLinker: hoisted now refreshes directories supplied by custom fetchers when reinstalling. pnpm also keeps the symlinks inside those directories.
With enableGlobalVirtualStore on, scripts can run entry points that a CommonJS require hook loads again, such as ts-node index.ts. They failed with ERR_UNKNOWN_FILE_EXTENSION on Node.js versions without built-in TypeScript support #16436.
pnpm now keeps each project's current lockfile and hidden hoisted dependencies in its own node_modules/.pnpm when virtualStoreDir points at a shared global virtual store. --virtual-store-dir now sets the global virtual store's location too pnpm/tasks#47.
pnpm clean no longer deletes the project when virtualStoreDir or globalVirtualStoreDir is set to the project directory. It also leaves a directory outside the project alone when the setting reaches it through a symlink. It now removes a global virtual store that globalVirtualStoreDir places inside the project, as it does for virtualStoreDir.
pnpm install no longer fails when a dependency of an optional dependency is missing from the registry. Like npm, pnpm now leaves out the nearest optional dependency above it, together with its subtree #16511.
When an optional dependency fails to build, pnpm now removes its link from node_modules. A repeat pnpm install then reports "Already up to date" and no longer reruns the failing build #16468.
pnpm install now prints a warning with the error when an optional dependency cannot be fetched and is skipped. The skipped package is no longer counted in the Packages: +N summary. The pnpm:skipped-optional-dependency log reports the skip with the fetch_failure reason #16514.
Fixed pnpm install changing an unchanged project's direct dependency to a sibling workspace's pinned version when its dependency tree contains a cycle #16417.
With autoDedupe enabled, downgrading a dependency in one workspace project now moves the other projects to that version when it satisfies their ranges. This also applies to a filtered pnpm --filter <project> add #16432.
pnpm install and pnpm dedupe now move an optional peer to the version already in the dependency graph when no other package provides its locked version anymore. After a bump such as vue 3.5.40 to 3.5.43, the lockfile kept a second copy of @vue/server-renderer for @vue/test-utils #16443.
pnpm dedupe --check no longer fails right after pnpm install when a project's optional peer is satisfied by a package another workspace project installs. pnpm dedupe now picks the same versions for that package's dependencies as pnpm install #16447.
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.
With autoDedupe enabled, pnpm install --lockfile-only no longer resolves the dependency graph again when nothing changed since an earlier --lockfile-only install deduplicated the lockfile. Such an install keeps the lockfile even if versions were published since it was written, or if only a setting such as resolutionMode changed. Run pnpm dedupe to apply such a change #16458.
A repeat pnpm install in a large workspace reports "Already up to date" faster #16487.
Sped up dependency resolution of workspaces with many peer dependencies.
pnpm install sends fewer registry metadata requests when the lockfile already decides which version a range resolves to. This now also covers ranges that several locked versions satisfy when one of them outranks the others, and direct dependencies kept at their locked version. Packages that minimumReleaseAgeExclude lists without a version now reuse cached registry metadata the same way they do when minimumReleaseAge is not set #16458.
pnpm no longer downloads every packument again on each install from a registry whose metadata responses forbid caching, such as Cache-Control: no-store. pnpm revalidates the cached metadata with a conditional request, so a registry that supports conditional requests answers with a 304 when the package has not changed #16528.
Cached metadata for a package published within minimumReleaseAge is now revalidated with its ETag, so the npm registry can answer 304 Not Modified. Before, the next install that checked the cache downloaded the whole document again #16506.
A fetch timeout while other downloads from the same host are still running now lowers concurrency for that host to one connection. Retries of that request, and later downloads from that host, use the lower concurrency. Other hosts keep the configured concurrency #12791.
Sped up pnpm install --offline when the version a range picks is not in the store. While it looks for a version the store holds, pnpm now reads only the versions the range admits #16495.
pnpm install --offline now reuses config dependency tarballs that are already present in the store pnpm/tasks#46.
pnpm -s <script> runs the script again, with -s meaning --sequential as it does for pnpm run -s <script>. pnpm rejected it with "unexpected argument '-s' found" #16446.
pnpm run and pnpm exec now warn and run the command when the install that verifyDepsBeforeRun starts fails. This lets scripts run in sandboxes where pnpm cannot install, such as containers with a read-only store or no network #15173.
A filtered pnpm run or pnpm exec now finds dependencies out of date when a workspace dependency of a selected project has no node_modules directory, as after a filtered install. With verifyDepsBeforeRun: install, pnpm installs that dependency before running the command pnpm/tasks#45.
Scripts run without a terminal no longer start a second sh each. One watchdog per pnpm command now ends every script's process group if pnpm is killed, so pnpm -r run across many projects starts half as many processes #16489.
Terminate batch job (Y/N)? no longer appears after pressing Ctrl+C in a script started with pnpm from PowerShell or cmd on Windows #16502.
pnpm rebuild and pnpm approve-builds refresh command launchers when a build changes a command's interpreter or replaces it with a native executable. Dependent packages' build scripts use the refreshed launchers.
When pnpm run <script> or pnpm <script> finds nothing to run and --filter follows the script name, the error now suggests putting the filter option before the script name #4655.
Package-name filters now support ? to match one character #2817.
pnpm self-updatepnpm no longer downloads the project's pinned pnpm version again on every command when nodeVersion in pnpm-workspace.yaml names a different Node.js major than the node on PATH. Before, each of those commands took about a second longer and failed without network access #16497.
Several pnpm commands started at once in a project that pins packageManager no longer fail with The process cannot access the file because it is being used by another process on Windows while the pinned pnpm is being installed.
pnpm can now switch to a packageManager version below 11 on x64 musl Linux, such as Alpine #16467.
A devEngines.packageManager range no longer makes pnpm replace the version recorded in pnpm-lock.yaml with the running pnpm while the recorded version still satisfies the range. When pnpm does record a version, it records the running pnpm only if it meets minimumReleaseAge. Otherwise it records the newest version in the range that meets it, or the running pnpm if none does #16431.
On Windows, pnpm self-update now replaces a pnpm.exe left in PNPM_HOME or in PNPM_HOME\bin. Windows ran that executable in place of the updated pnpm.cmd shim, so pnpm --version kept printing the old version after a successful update. If the executable was in PNPM_HOME, self-update now asks you to run pnpm setup #9094.
pnpm self-update now checks that a version installed as the JavaScript pnpm can start before making it the global pnpm. If Node.js is missing, the update fails and the current pnpm stays in place.
pnpm deploy now finds patches and local dependencies when the target directory sits under a symlink, such as /tmp on macOS. It failed with ERR_PNPM_PATCH_NOT_FOUND #16470.
pnpm deploy --legacy now resolves the deployed project's relative file:, link:, and path dependencies from the project's own directory #16475.
pnpm update --global now removes hard-linked executables from PNPM_HOME when migrating packages from the old global layout #16420.
pnpm store prune no longer fails on store index entries that pnpm 11 wrote for git-hosted packages without a package.json. Entries that still cannot be read are kept and counted in the prune summary.
pnpm store prune now aborts when an error other than a missing directory occurs while scanning project directories in the mark phase.
pnpm config get and pnpm config list now report a setting given on the command line with --config.<name>=<value>. Before, a value such as --config.node-linker=hoisted reached the install but was absent from the reported configuration #16276.
pnpm -r pkg get now reports every selected project when several share a package name. Projects with the same name are keyed by their directory relative to the workspace root. Before, only one of them appeared in the output.
The install summary shows a link: dependency as + name <- path, and the Node.js API's hideLinkedPkgsDiff reporter option leaves matching linked dependencies out of the summary.
The --force help text of pnpm install and pnpm add now says that --force keeps skipping optional dependencies built for other platforms. It points to forceIgnoresPlatform and the --os, --cpu, and --libc options for installing them #16435.
The homepage field of the published pnpm package points to https://pnpm.io again.
pnpm 12.8.2 fixes a startup crash on Linux ppc64le and UnknownIssuer errors on systems without CA certificates. pnpm run no longer installs before eve
pnpm 12.8.2 fixes a startup crash on Linux ppc64le and UnknownIssuer errors on systems without CA certificates. pnpm run no longer installs before every script on CI when autoDedupe is enabled, and resolution and hoisted installs on macOS are faster.
Fixed pnpm crashing on startup on Linux ppc64le #16380.
Fixed installs failing with UnknownIssuer on Linux systems without CA certificates, such as node:24-slim, when NODE_EXTRA_CA_CERTS is set. The extra certificates now extend the bundled CA roots #16365.
pnpm now creates its store operation locks and other per-user lock files in $XDG_RUNTIME_DIR when it points to a directory only the user can write to. Otherwise, pnpm still uses /tmp on Linux and macOS. Sandboxes that block writes to /tmp can point XDG_RUNTIME_DIR at a writable directory #16390.
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 rejected the option before switching, so pnpm install --auto-dedupe failed with "Unknown option" even though the pinned pnpm supports it #16353.
pnpm install --frozen-lockfile now fails when Cargo.lock does not satisfy a dependency requirement in Cargo.toml. The error names the crate and the version the lockfile holds #16355.
pnpm install returns "Already up to date" again in a workspace with injected workspace dependencies and a shared lockfile. Since 12.7.0 every repeat install in such a workspace ran the full install and copied the injected projects again.
With injectWorkspacePackages: true, a fresh pnpm install now records a workspace dependency as link: when its injected copy differs from the project only by an optional peer that peer-dependent dedupe merges. It was recorded as a peer-suffixed file: copy #16354.
pnpm dedupe --check now passes right after pnpm dedupe when deduplication merges variants of a package that differ only in their peers. A lockfile key whose peer suffix named a merged variant now names the variant that replaced it #16356.
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. Before, while a new 1.0.0 was too new, latest fell back to an old 0.0.1 even though 1.0.0-beta.4 had been latest until then #16388.
Git-hosted dependencies now respect pmOnFail. If it is set to anything other than download, a git-hosted dependency that pins a pnpm version is prepared by the running pnpm, and pnpm does not download the pinned version #16376.
pnpmfile hooks such as readPackage now run once for a dependency that several packages request at the same time. They could run twice for it before.
childConcurrency now defaults to 5, the documented value. It used to be capped at 4 and to follow the host's CPU count.
pnpm run and pnpm exec no longer install dependencies before every script on CI when autoDedupe is enabled. pnpm install --frozen-lockfile now keeps the deduplication record left by an earlier install #16374.
On macOS and Linux, lifecycle scripts and pnpm run now always get PATH from the PATH variable. When the environment also held a Path variable, a script sometimes got Path's value, and failed with node: not found #16308.
pnpm run now exits after a SIGTERM in a container where pnpm is PID 1 and the script runs pnpm again, as "start": "pnpm serve" does. Since 12.6.0 it kept waiting after the script had shut down, until the container runtime killed it.
pnpm config get globalShims, pnpm shim list, and global installs no longer read globalShims from a project's pnpm-workspace.yaml. Only the global config file, the pnpm home's own pnpm-workspace.yaml, and PNPM_CONFIG_GLOBAL_SHIMS set it, so a repository cannot choose which globally installed packages get project-aware shims.
pnpm config set --location=project refuses a machine-level setting such as stateDir or scope with ERR_PNPM_CONFIG_SET_NOT_A_PROJECT_SETTING, which names where the setting belongs. pnpm config delete still clears such a key from a project's pnpm-workspace.yaml.
pnpm deploy no longer fails with ERR_PNPM_DEPLOY_AMBIGUOUS_PEER in a workspace with injectWorkspacePackages: true when a workspace package lists its peer dependency as a dev dependency too #16375.
pnpm deploy no longer copies the workspace root's packageManager and devEngines.packageManager fields into the deployed package.json #16403.
pnpm publish now includes bare README files and README files with Markdown extensions such as readme.markdown in registry metadata #12704.
pnpm store prune now removes the packages that only expired pnpm dlx cache entries used. They were left in the store until the next pnpm store prune #16383.
Sped up dependency resolution in large workspaces, and when many dependencies request different ranges of the same package. Resolution also uses less memory.
Sped up pnpm install with nodeLinker: hoisted on macOS when the lockfile is re-resolved, such as with autoDedupe enabled #16397.
Sped up extracting package tarballs.
pnpm install without --frozen-lockfile is faster on some machines in projects with a pnpm-workspace.yaml. Those installs linked with one worker thread per core, half of what a frozen install uses.
On Windows, warm pnpm install --frozen-lockfile runs are 4-5% faster on 4- and 8-core machines. pnpm now links with one worker thread per core on Windows, between 4 and 16. This changes frozen installs and installs in projects without a pnpm-workspace.yaml on machines with 3 to 15 cores.
pnpm 12.8.1 fixes pnpm install --frozen-lockfile rejecting lockfiles with injected workspace packages that have peers, restores the executable bit on
pnpm 12.8.1 fixes pnpm install --frozen-lockfile rejecting lockfiles with injected workspace packages that have peers, restores the executable bit on files of local directory dependencies, makes pnpm dedupe converge, and uses less CPU on many-core machines.
pnpm install --frozen-lockfile no longer rejects a freshly generated lockfile when an injected workspace package has peer dependencies #16332.
Executable files in a file: directory dependency or an injected workspace package keep their executable bit again. Since 12.8.0, pnpm installed these files without the permissions they have in their project.
pnpm dedupe now reaches a stable lockfile when a package's peer suffix is long enough to be hashed. Before, each run could switch that package's key between the hashed and the spelled-out suffix, so pnpm dedupe --check always failed #16331.
pnpm install --frozen-lockfile, the default in CI, now uses less CPU on machines with more than 8 cores. Warm installs on many-core Windows machines got up to 10% faster. Frozen installs now link with at most 16 worker threads.
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 update -g --latest now upgrades globally installed packages beyond their saved version ranges #16320.
pnpm add --save-types no longer adds a @types/* package whose resolved version is deprecated. DefinitelyTyped publishes such stubs for packages that s…
pnpm 12.8.0 warns when pnpm pack or pnpm publish would ship a .env file that files does not list, installs sharedWorkspaceLockfile: false workspaces concurrently, applies every setting passed as --config.<name>=<value>, and no longer leaves the Windows terminal stuck after Ctrl+C in a script.
pnpm pack and pnpm publish now warn when the tarball includes a .env or .env.* file that the files field of package.json does not list. Templates such as .env.example are not reported. List the file in files to publish it on purpose, or exclude it in .npmignore or .gitignore #7826.
pnpm pack now honors --silent, --reporter=silent, and --loglevel=silent to hide the tarball contents and summary. With --json, lifecycle script output and the final JSON output remain visible #10297.
Installing through a pnpr server now records the pnpmfile checksum in the lockfile, so a later pnpm install --frozen-lockfile accepts that lockfile #14460. 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.
Installing through a pnpr server also links a workspace project at the directory its publishConfig.directory names. A server that does not forward the setting makes the install fail with ERR_PNPM_PNPR_PUBLISH_DIRECTORY_MISMATCH, so pnpm never writes a lockfile that points at the wrong directory. The server rejects a publishConfig.directory that points outside its project.
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.
A git-hosted dependency that is a pnpm workspace with no committed lockfile is now detected as a pnpm project #14011.
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 --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. The error also carries the ERR_PNPM_NO_OFFLINE_META code. pnpm cache prune --help now says that pnpm 11.26 and earlier, and pnpm 12.3 and earlier, depend on the directories it removes #15656.
Running pnpm install now refreshes dependencies when a package declared with a local file: directory changes its dependencies #4623.
A repeat pnpm install now keeps its fast up-to-date check when an override replaces a declared local file: dependency #12892.
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.
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. On Windows, the install also no longer fails with "Access is denied" when another project's copy of a shared dependency links to the deleted directory.
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.
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 write.
pnpm install no longer aborts on a failed allocation of many gigabytes when peer dependency ranges combine overlapping || alternatives #15867.
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.
An npm: alias written by overrides now stays in place when a change elsewhere makes pnpm re-resolve the aliased dependency. Before, pnpm could look up the alias name at the aliased version, which failed with ERR_PNPM_NO_MATCHING_VERSION or locked an unrelated package #16309.
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 dedupe no longer changes the lockfile on every run when a nested peer dependency is provided through an npm alias #15709.
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.
pnpm install retries registry metadata fetches that fail with a timeout, a dropped connection, or an interrupted response body before it applies trustPolicy or minimumReleaseAge. A transient fetch failure is not reported as TRUST_DOWNGRADE or MINIMUM_RELEASE_AGE_VIOLATION #12031.
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.
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.
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.
pnpm install --frozen-lockfile now works on a detached HEAD when gitBranchLockfile is enabled. The install 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 --frozen-lockfile now accepts a lockfile that has no importer entry for a workspace package without dependencies. Such a package added after the lockfile was written made the install fail with ERR_PNPM_PACKAGE_MANAGER_NO_IMPORTER #15875.
pnpm install now fails with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY when an importer references a dependency version that has no snapshot entry. Before, the install succeeded and left a node_modules symlink pointing at a missing virtual-store directory #14764.
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.
With gitBranchLockfile enabled, each emoji or other character outside the Basic Multilingual Plane in a branch name now becomes !! in the lockfile name. Before, each such character became one !.
pnpm install in a workspace with sharedWorkspaceLockfile: false now installs projects concurrently, up to workspaceConcurrency at a time #14480. A project is resolved, fetched, and written to its virtual store without waiting for the workspace projects it depends on. It waits for them only before it links its dependencies and runs its lifecycle scripts, so its scripts still run after theirs. A project with a preinstall or pnpm:devPreinstall script, or with an injected or file: workspace dependency, waits for its workspace dependencies before it starts.
The installs of the projects also share their package metadata, lockfile verification, and store caches, so they use less CPU and memory when several projects depend on the same packages. An install with a pnpmfile no longer starts an extra Node.js process when the pnpmfile has no preResolution hook.
With enableGlobalVirtualStore and sharedWorkspaceLockfile: false, each project now keeps its current lockfile and its hidden hoisted dependencies in its own node_modules/.pnpm. Before, every project wrote them to the workspace root's node_modules/.pnpm, so each repeat install treated the other projects' packages as its own and relinked them #14480.
pnpm rebuild, pnpm approve-builds, and pnpm ignored-builds now work on the current project's node_modules when they run inside a project of a workspace with sharedWorkspaceLockfile: false. They used to read the workspace root's node_modules, so pnpm rebuild did not rebuild the project's dependencies and created a second virtual store at the workspace root #9402.
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. It also no longer fails with ERR_PNPM_CMD_SHIM_RESOLVE_PATH when such a package has a bin field and its publishConfig.directory does not exist yet.
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.
An in-place edit to the source of an injected workspace package now shows up in its injected copy, unless a build writes to that package or packageImportMethod is set. pnpm hardlinks such packages under the default import method #4410. Scripts listed in syncInjectedDepsAfterScripts now update injected dependencies while they run, so a watcher on the injected package, such as a dev server, sees each change before the script exits.
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.
Workspace discovery prunes dot-prefixed directories, so a packages pattern such as ** no longer matches projects inside .cache and other hidden directories #16250.
pnpm import in a workspace now keeps the versions pinned by a yarn.lock inside a workspace project #4385.
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.
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, so hard-linked copies in other projects are healed at the same time. On Windows the repair still replaces the file, so other projects are healed on their next install #3445.
pnpm install now reports a full store at once when writing package files fails. It no longer retries the tarball #8581.
pnpm now warns when it cannot hard link packages from an existing store in the pnpm home directory and falls back to 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.
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.
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.
With enableGlobalVirtualStore, an install into a fresh node_modules no longer runs the build scripts of a dependency whose global virtual store slot an earlier install already built. pnpm rebuild still runs them #14480.
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.
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 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 install honors Cache-Control for dependencies named with an http: or https: tarball URL. A fresh response is taken from the store with no request, and a stale one is revalidated with If-None-Match #15648.
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. Before, pnpm 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 with nodeLinker: hoisted now applies a patch once to each copy of a patched dependency in a workspace. Before, a copy that several workspace projects shared could receive the patch twice and end up with the patched content duplicated #7565.
pnpm install and pnpm fetch now fail with ERR_PNPM_PATCH_NOT_FOUND when a patch file listed in patchedDependencies does not exist #5268.
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 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 add --save-types no longer adds a @types/* package whose resolved version is deprecated. DefinitelyTyped publishes such stubs for packages that ship their own types, such as @types/typescript for typescript #15636.
pnpm version, pnpm add, and pnpm pkg set keep JSON5 style when they update package.json5. ASCII identifier keys stay unquoted, strings keep JSON5 quotes, and indented files keep trailing commas #15717.
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.
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 -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.
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 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.
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.
With enableGlobalVirtualStore, dependency build scripts now see the workspace root's node_modules/.bin, as they do with a local virtual store. A postinstall script that runs node finds the Node.js installed by devEngines.runtime and no longer fails with "command not found" on machines without a system Node.js #15652. Dependency build scripts also see the bins of privately hoisted dependencies.
Dependency install scripts now find the node-gyp bundled with pnpm when pnpm runs through a symlink, such as node_modules/.bin/pnpm or the pnpm that npm install -g pnpm links. They used to fail with node-gyp: command not found on macOS #15694.
pnpm run and lifecycle scripts now set npm_config_node_gyp to the bundled node-gyp entry point. Tools that read the variable resolve the same node-gyp pnpm builds with. An npm_config_node_gyp value the environment already sets is kept as is #16270.
Scripts now see the npm_command environment variable that npm sets. It holds run-script when the command runs a script, and the command's own name otherwise #16265.
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.
pnpx --version and pnpm dlx --version now print the pnpm version. Other unknown options before the command are reported as errors. Before, pnpm tried to download a package named after the option #16259.
pnpm dlx now keeps the virtual store of its cached installs in node_modules/.pnpm, like every other install #13955. pnpm pack-app now names the manifest of its runtime install directory pnpm-pack-app-<target>.
pnpm pack and pnpm publish now ship a file that the files field names even when another entry excludes the directory holding it. For example, ["**", "!dist", "dist/index.d.ts"] ships dist/index.d.ts #16213.
pnpm pack prunes a directory that a files field exclusion names, such as !**/test, excluding the directory and its contents from the packed package #15738.
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 --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 with a shared lockfile now copies workspace dependencies into the deploy directory, even when packageImportMethod is set to hardlink. Before, their files were hard-linked to the workspace sources, so editing a source file also changed the deployed copy #12176.
pnpm deploy --legacy no longer leaves broken links to nested local dependencies of workspace packages #9575.
Every setting pnpm supports can now be set with --config.<name>=<value> on the command line, not only the ones whose command also carries a matching flag. Before, pnpm install --config.frozen-lockfile=true dropped the setting and rewrote pnpm-lock.yaml as though the install had not been frozen #16276.
Settings given on the command line, such as --registry and --store-dir, now take precedence over the values a pnpmfile updateConfig hook sets #14063.
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.
pnpm now fails with ERR_PNPM_AUTH_INVALID_BASE64 when a registry's _password in .npmrc is not valid base64. Before, it sent the value as the raw password. A username or _password left empty, for example by an unset environment variable, now supplies no credential #16273.
proxy=false now turns proxying off even when HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY is set. pnpm no longer sends requests through a proxy named only in ALL_PROXY.
pnpm install now runs the install hooks of a config dependency plugin's pnpmfile, including readPackage, afterAllResolved, and custom resolvers. Its pnpmfile is also counted in pnpmfileChecksum. Before, only the plugin's updateConfig hook ran, so a plugin could not change the resolved dependencies.
A pnpmfile fetchers hook now runs once per package on a fresh install when it handles a resolution with a custom type or delegates a git-hosted one to the same subdirectory #15584. These packages were fetched a second time for installation, so the installed files could come from a different archive than the one their dependencies were read from. The hook also no longer runs twice when a resolvers hook returns a tarball resolution without a manifest #15025.
pnpm install now re-fetches a package from a custom resolver when the integrity of its resolution changes, with or without enableGlobalVirtualStore. It used to update the lockfile but keep the old files in node_modules #15670.
pnpm install now rejects invalid results from a readPackage hook. A hook that returns a non-object value fails with ERR_PNPM_BAD_READ_PACKAGE_HOOK_RESULT #15730. A hook that sets a dependency range to a value other than a string, such as undefined, fails with an error that names the dependency, the package and the pnpmfile. Delete the property to remove a dependency #15705.
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.
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.
On arm64 musl Linux, such as Alpine on ARM, switching to a pinned pnpm older than 12 now runs the JavaScript pnpm package. The standalone executable of those versions crashed at startup on that platform #10443.
Global shims such as node now work when pnpm runs through a relative symlink, as with a Homebrew install. They were copies of that symlink and did not resolve from the global bin directory #15691.
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.
Interrupting a script with Ctrl+C on Windows no longer leaves the terminal stuck #14860. A script that runs through a batch shim, as vite dev does through vite.CMD, made cmd.exe wait forever on its "Terminate batch job (Y/N)?" answer, and every following keystroke went to that prompt. pnpm now ends a cmd.exe script shell once it has sat for a second after the interrupt with nothing running under it. A script that takes longer to shut down is still waited for. A second Ctrl+C ends the script's shell at once.
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.
The Windows pnpm.exe runs on a clean Windows install that does not have the Visual C++ Redistributable. It used to exit immediately on startup because that runtime was missing #15723.
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. Command shims also run tools whose paths contain non-ASCII characters #6999, including the PowerShell shims in Windows PowerShell 5.1 #16217.
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.
On Windows, installing pnpm with npm inside a project now writes node_modules/.bin shims that run pnpm.exe. A global install with npm install --location=global now gets the same shims as npm install -g #15688.
pnpm install no longer fails with ERR_PNPM_WORKSPACE_INVALID_GLOB on Windows for a wildcard pattern such as plugins/*/* in pnpm-workspace.yaml when the workspace is on a different drive than the pnpm cache or state directory #16239.
On Windows, pnpm install no longer skips a dependency's build script on a later install when the package ships an executable file and the script changes nothing inside the package directory #15667.
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.
pnpm setup on Windows no longer panics when an unrelated environment variable has a name containing a non-ASCII character. It skips that variable #15684.
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. Before, it showed the directory pnpm wanted to set.
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.
pnpm audit and pnpm audit signatures now fail with an error when the lockfile contains unresolvable dependency references #13638.
pnpm licenses list now reports the actual on-disk package locations when using nodeLinker: hoisted or shamefully-hoist: true #8589. With --json, its paths array now includes every installed copy of a package, including hoisted copies and isolated installations with different peer dependencies.
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.
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.
When a dependency fails to resolve, the error now shows the cause. For example, a Node.js runtime download behind a proxy that re-signs TLS now reports invalid peer certificate: UnknownIssuer #9556.
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.
Lockfile verification now fails with ERR_PNPM_TARBALL_URL_MISMATCH, ERR_PNPM_TARBALL_REVISION_MISMATCH, or ERR_PNPM_MISSING_NAMED_REGISTRY when every rejected entry failed that check. These failures were reported as the generic ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION.
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 install no longer prints an extra Progress: line after the progress line is marked done #16184.
pnpm install --force no longer installs optional dependencies built for other platforms. This release also carries security fixes for bin shims on Nix…
pnpm 12.7.0 ships with .nvmrc and .node-version support in the global node shim, pnpm install --allow-build, pnpm publish --publish-wait-timeout, and pnpm-workspace.yaml created from the workspaces field. pnpm install --force no longer installs optional dependencies built for other platforms. This release also carries security fixes for bin shims on Nix, for lifecycle scripts of packages in a storeDir inside the workspace, and for userAgent placeholders in pnpm-workspace.yaml.
pnpm install --force now keeps skipping optional dependencies whose os, cpu or libc do not match the host. It still refetches every package and lifts engineStrict. The new forceIgnoresPlatform setting restores the previous behaviour, installing optional dependencies of every platform under --force #6133.
The global node shim created by pnpm now uses the Node.js version from the nearest .nvmrc or .node-version file when the project does not declare a Node.js runtime in devEngines.runtime or engines.runtime #4471. The nearest directory with a Node.js runtime declaration decides the version. Within one directory, package.json takes precedence over .node-version, which takes precedence over .nvmrc. An .nvmrc value that only nvm can act on, such as system or a custom alias, is ignored.
pnpm install now supports the --allow-build option to selectively allow or deny package lifecycle scripts and record them in pnpm-workspace.yaml #15388.
Added pnpm publish --publish-wait-timeout <milliseconds> to wait for published versions and their tarballs to become available from the registry. Set publishWaitTimeout in pnpm-workspace.yaml to configure a default. A value of 0 disables the check.
Recursive publishing confirms availability before publishing dependent packages. If confirmation times out, the command fails.
When pnpm publish -r --report-summary fails after some uploads were accepted, the summary file now lists those packages.
pnpm install now creates pnpm-workspace.yaml from the workspaces field of the root package.json when the repository has no pnpm-workspace.yaml. The projects the field lists are linked on that same install. An existing pnpm-workspace.yaml is never changed. With --ignore-workspace, no file is created. If the workspaces field later differs from packages in pnpm-workspace.yaml, pnpm prints a warning #2255.
When a project pins a pnpm version or a runtime that another pnpm process is installing at that moment, pnpm now waits a few seconds and then installs and runs a private copy of its own. It used to wait up to five minutes and then use the shared install directory without the lock. The private copy is removed once the command has run. pnpm store prune removes any private copy that a killed process left behind #15413.
pnpm now keeps the blank lines between entries of package.json when it updates the file, for example on pnpm add #5602.
pnpm no longer expands environment variables in a userAgent set in a project's pnpm-workspace.yaml. A userAgent with a placeholder in that file is now ignored. Before this fix, pnpm sent the variable's value to the configured registry #15415.
On Nix, a dependency's bin named like a system utility such as sed can no longer redirect a POSIX bin shim or the pnpm, pn, pnpx, and pnx launchers. The shims and launchers now ignore node_modules and relative PATH entries while they locate their own files. Installing again replaces the shims already in node_modules #14883.
pnpm no longer treats manifests inside its store, cache, state, or modules directories as workspace projects. Before, a storeDir inside the workspace could let lifecycle scripts of packages in the store run without allowBuilds approval #15033.
Packages that run a lifecycle script are no longer hard-linked into the virtual store, so a build script can no longer rewrite the workspace source of an injected package or the store copy it was imported from #15483.
Fixed pnpm install, pnpm add, pnpm remove, and pnpm peers check running out of memory when many packages share a missing peer dependency. This mostly affected projects with autoInstallPeers: false #15362.
pnpm no longer hangs for up to 5 minutes after a pnpm process was killed while setting up the pnpm version pinned in packageManager or devEngines #15360, #15393. The killed process left behind a lock that every later pnpm command in the project waited on. pnpm now detects that the process holding a lock is gone and takes the lock over at once. The same applies to the locks pnpm takes while installing a managed runtime or writing the global bin directory. Two pnpm processes that are both still running keep waiting for each other as before.
Requests to a registry or tarball server whose TLS certificate fails verification now fail at once. Such requests were retried for more than a minute without any output #9134.
On macOS, pnpm now falls back to its bundled CA roots when system trust evaluation is unavailable, such as in a sandbox or when macOS cannot create an SSL policy for a registry connection. Installs failed or crashed on the first registry request in that case. Custom ca certificates are now honored directly #15329, #14461.
pnpm install now caps concurrent connections to a proxy at 50 sockets by default #15280. It also immediately retries transient connection resets when downloading package archives.
pnpm install now reuses a package already present in the store when an existing lockfile entry satisfies the dependency, avoiding registry requests that fail without authorization #2522.
Installing or adding dependencies no longer fails when a previously installed local tarball file was deleted from disk #8367.
pnpm install now installs the new version of a local tarball dependency whose file was replaced at the same path #2437. pnpm install --frozen-lockfile rejects such a changed tarball, even when the previous archive contents are in the store #1889.
pnpm install now fetches committed submodules of git dependencies #1470.
pnpm install now applies patches produced by pnpm patch-commit when an edit removes the trailing lines of a file along with its newline. The install no longer fails with ERR_PNPM_INVALID_PATCH ("expected end of hunk") #12451.
pnpm install now preserves existing node_modules directories when a cross-device move reports EXDEV #14504.
pnpm install no longer fails when writing the workspace state file encounters an error. Failures to update the state file now emit a warning instead of aborting the install #14550.
Interrupting pnpm install with Ctrl+C or SIGTERM no longer leaves a temporary lockfile (.pnpm-lock.yaml.*.tmp) behind in the project #1418.
pnpm install now relinks a direct dependency whose link in node_modules points to a missing target. Before, it reported "Already up to date" and left the broken link #9758.
pnpm install uses less CPU when it links packages from a warm store. On Windows, a warm install could take several times longer than with pnpm 11 #15439.
pnpm install now runs node --version once per run. A workspace whose projects keep their own lockfiles (sharedWorkspaceLockfile: false) previously ran the probe once or twice for every project, and on macOS the concurrent launches waited on each other, so a project could wait several seconds before its linking started.
A repeat pnpm install --frozen-lockfile with nodeLinker: hoisted in a workspace no longer re-links node_modules when nothing changed.
Custom fetcher hooks no longer run a second time during installation when an archive was already fetched during dependency resolution #15025.
Fixed a package resolved by a resolvers pnpmfile hook installing without its own dependencies. This happened when the hook returned no manifest and a fetchers hook handled the resolution #15552.
pnpm install --prod and other installs that skip devDependencies no longer run the pnpm:devPreinstall script #7065. They skip prepare lifecycle scripts too, as do installs given package arguments.
pnpm prune --prod and production installs now remove devDependencies when lockfile: false is configured #2677.
pnpm install --prod, pnpm fetch --prod and pnpm deploy --prod no longer install a devDependency that is only there to satisfy an optional peer dependency of a production dependency. pnpm list, pnpm why, pnpm licenses, pnpm sbom and pnpm audit leave it out of --prod results too. The same applies to --dev. A peer that is not optional is still installed and audited #15344.
pnpm install no longer skips optional dependencies that the Node.js version locked for a devEngines.runtime range supports, when the range uses onFail: download. An explicitly set nodeVersion still takes priority #14628.
pnpm fetch now also installs the pnpm version that pnpm-lock.yaml pins, when it differs from the running pnpm. A later pnpm install --offline that switches to the pinned version no longer fails because that version is missing from the store #11808.
A dependency that ships a binding.gyp and sets gypfile: false no longer gets the node-gyp rebuild install script pnpm synthesizes for it. Such a dependency needs no allowBuilds entry and is no longer listed under "Ignored build scripts".
pnpm install no longer adds allowBuilds placeholder entries to pnpm-workspace.yaml when it runs in CI or without a terminal. Interactive installs still add them #11574.
pnpm now detects the same CI environments as pnpm 11, including AWS CodeBuild, which does not set CI. On these services pnpm install uses a frozen lockfile by default and fails with ERR_PNPM_OUTDATED_LOCKFILE when the lockfile is outdated.
Installing through a pnpr server now installs a project's peer dependencies when autoInstallPeers is enabled. A project that declared only peer dependencies failed with ERR_PNPM_OUTDATED_LOCKFILE or skipped its peers #14833.
pnpm now installs a dependency that a package also declares as an optional peer dependency, for example lightningcss in some vite builds. The dependency was missing from node_modules, so the package failed to import it #8912.
Removal overrides such as "parent>peer": "-" now prevent optional peers from being installed from another workspace package #15008.
Removing an entry from overrides now re-resolves the packages it targeted. A version the override had locked is no longer kept just because the declared range still accepts it #4587.
packageExtensions and overrides entries with a ranged selector (such as @<X or @*) no longer match a dependency that has no package.json, such as a local directory dependency #15007.
Trim leading and trailing whitespace from dependency override selectors in pnpm.overrides #6356.
With trustPolicy: no-downgrade, pnpm now resolves the newest matching version that is not a trust downgrade. Previously a dependency failed with ERR_PNPM_TRUST_DOWNGRADE even when an older version satisfied its range. pnpm self-update picks its target version the same way. A request for an exact version still fails #14176.
pnpm install now re-resolves a dependency when its manifest range is updated from a prerelease to a stable version. The lockfile previously retained the prerelease version and caused --frozen-lockfile to fail #15528.
pnpm install --ignore-pnpmfile no longer removes pnpmfileChecksum from an up-to-date pnpm-lock.yaml. pnpm install --frozen-lockfile --ignore-pnpmfile no longer fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH when the lockfile records a pnpmfileChecksum. A command that resolves dependencies with the pnpmfile ignored still writes the lockfile without it #10944.
pnpm install and pnpm peers check now use local tarball packages' actual versions when checking peer dependencies. Compatible packages no longer fail with strictPeerDependencies enabled.
pnpm peers check and the install-time peer dependency check now resolve peer dependencies from the workspace root when resolvePeersFromWorkspaceRoot is enabled #14982.
autoDedupe and pnpm dedupe now move transitive dependencies to the version a catalog: dependency pins, as they already did for versions written directly in package.json. Previously they could move those dependencies to a higher version and keep both versions in the lockfile.
pnpm dedupe now produces a stable lockfile when a dependency's range matches both a direct dependency and an npm: alias of the same package. The dependency resolves to the version of the direct dependency. Repeated runs previously alternated between two lockfiles #15588.
Merging lockfiles now preserves recorded configuration fields such as overrides, neverBuiltDependencies, patchedDependencies, packageExtensionsChecksum, settings, and catalogs #8366.
A lockfile entry whose resolution is unchanged now keeps its recorded deprecated message #5772.
pnpm no longer writes a package's legacy array-form engines, such as ["node >= 0.8"], to the lockfile. It was recorded as an object keyed by index, such as {'0': node >= 0.8} #4518.
Tarball URLs recorded in the lockfile now strip default HTTP and HTTPS ports (:80 and :443) #15539.
node_modules/.package-map.json no longer contains entries that point at directories that do not exist. Such entries appeared for packages installed only with peer dependencies, most visibly with enableGlobalVirtualStore #14938.
With nodeLinker: hoisted, hoistWorkspacePackages now links each workspace project that hoistPattern or publicHoistPattern selects into the root node_modules, unless a hoisted package or a root dependency already uses its name. The project's bins are linked into the root node_modules/.bin #7553.
With nodeLinker: hoisted, pnpm install now removes the commands of the packages it removes from node_modules/.bin, such as a nested copy deduped into the root node_modules #7568.
pnpm install no longer puts a dependency's bin on PATH for that dependency's own lifecycle scripts before the bin's file exists. pnpm links such a bin after the dependency's build has run. It also removes such a bin left by an earlier install. This fixes installing the node package on Windows #15501.
Dependencies and executable binaries are now correctly linked and accessible for workspace packages using publishConfig.directory and publishConfig.linkDirectory #8338.
Bin linking leaves workspace and linked dependency files outside node_modules unchanged. Already executable bin files no longer receive redundant permission changes.
pnpm install now finds workspace projects reached through a symlink, such as a packages directory that links to a folder outside the workspace. It installs their dependencies, and the links in their node_modules resolve #1044.
A dependency declared with catalog: now counts as a workspace dependency when its catalog entry points at a workspace project, for example workspace:* #15587. With linkWorkspacePackages enabled, so does an npm: alias of a workspace project, such as "math-alias": "npm:math@^1.0.0". pnpm -r run runs that project first. --filter <pkg>... selects it.
A workspace: dependency now resolves to a workspace project whose version is not valid semver, such as 1 or 1.0. workspace:*, workspace:^, and workspace:~ match it. A range identical to the version also matches it #4567.
A workspace: dependency with an exact version now resolves to a workspace project whose version carries SemVer build metadata. For example, workspace:0.5.6-next.3 matches a project at 0.5.6-next.3+f60facc #6483.
Secondary dependencies now prefer the version resolved by the local project's direct dependencies over versions from sibling workspace projects #7191.
pnpm install now re-resolves a workspace project's auto-installed peer dependency when another workspace project changes its specifier for that package to one that excludes the locked version but still overlaps the peer range. The peer then resolves to the version a fresh install would pick #11800.
pnpm install --frozen-lockfile now fails with ERR_PNPM_OUTDATED_LOCKFILE when pnpm-lock.yaml lists a workspace project whose directory or manifest file is missing. The install used to report success without installing that project's dependencies #7667.
pnpm install -r now installs every workspace project when recursiveInstall is set to false in pnpm-workspace.yaml #7504.
pnpm install with --filter now installs only the dependencies of the selected projects when using nodeLinker: hoisted #8882.
pnpm install now updates an injected workspace dependency after that package's own dependencies change, when shared-workspace-lockfile is false #7209.
pnpm install now copies the output of a workspace package's own prepare, install, or postinstall script into the injected copies of that package. Before, the injected copies kept only the files that existed before the script ran. syncInjectedDepsAfterScripts now also works when modulesDir is set #9464.
syncInjectedDepsAfterScripts now copies files into injected dependencies when node_modules is on another filesystem than the package sources. The sync previously failed with a cross-device link error and made the script run exit with an error #14703.
A modulesDir with several path segments, such as www/modules, now puts each workspace project's dependencies in <project>/www/modules on both fresh and frozen installs, and pnpm bin prints <project>/www/modules/.bin #15484.
With nodeLinker: hoisted, pnpm now installs the root project's dependencies into a custom modulesDir instead of node_modules. With a custom modulesDir, the virtual store and its lock.yaml now default to <modulesDir>/.pnpm.
A repeat pnpm install in a workspace with a custom modulesDir now takes the up-to-date fast path. Before, pnpm looked for each workspace project's dependencies in node_modules and ran a full install every time.
pnpm now warns when a workspace install covers a project that has its own pnpm-workspace.yaml. The nested file's settings, such as patchedDependencies, do not apply when the outer workspace installs that project. pnpm reads settings only from the pnpm-workspace.yaml at the workspace root #11724.
The [<since>] filter selector now compares against the commit where the current branch forked from <since>. Projects changed only by newer commits on <since> are no longer selected. Uncommitted changes are still included. In a shallow clone without that commit, pnpm compares against <since> directly, as before #9907.
--filter "[<since>]" now selects workspace packages when dependency versions change in a catalog in pnpm-workspace.yaml #8718. It also selects projects that files were moved out of when git detects the move as a rename #15481.
--filter now evaluates selectors in order, so later inclusion filters can re-include packages that an earlier exclusion filter excluded #9354.
pnpm add now saves changes to package.json before running lifecycle scripts, so a postinstall script failure leaves the added dependency in package.json #8627.
pnpm add now saves the requested exact version when adding a dependency, even when the manifest already contains a version range #6040.
pnpm add <pkg>@<version> and pnpm update <pkg>@<version> now move the catalog entry onto the named version when the entry's range already covers it. For example, ^7.22.17 becomes ^7.29.6, the same way pnpm update <pkg> moves an entry to the version it resolves #13715.
pnpm add and pnpm install keep an empty peerDependencies, dependencies, devDependencies, or optionalDependencies field that was already in package.json. pnpm still drops such a field when it removes the last entry itself, as pnpm remove does #5096.
pnpm update now keeps a version range whose shape has no save prefix, such as <= 3.0.0 or >=1.0.0 <2.0.0, when the updated version still satisfies it. Before, <= 3.0.0 became ^3.0.0 #6714.
pnpm update <pkg> now moves a package off a locked version the registry no longer serves, such as an unpublished release. The lockfile check for supply-chain policies such as minimumReleaseAge used to reject that version before the update could replace it #9953.
pnpm update --prod no longer installs devDependencies when run in a project installed with --prod #8038.
pnpm update --interactive --workspace now allows external dependencies to be updated.
pnpm outdated and pnpm update now apply minimumReleaseAge to GitHub Actions. minimumReleaseAgeExclude entries match action names such as actions/checkout #13923.
pnpm remove now accepts --trust-lockfile and --no-trust-lockfile to control supply-chain policy checks while removing a package #14406.
pnpm unlink now removes the link: dependency that pnpm link <dir> added to package.json. The linked package is removed from node_modules and the lockfile. A link: dependency to another directory is kept #4219.
pnpm install now prunes unreferenced catalog entries from pnpm-workspace.yaml when catalogPrune: true is configured #15273.
minimumReleaseAgeExcludePrune and trustPolicyExcludePrune now work in workspaces with shared-workspace-lockfile=false. Once every project has been installed, pnpm drops an entry only if no project lockfile records it. Undecided allowBuilds entries are pruned the same way #14612.
Exclude entries that pnpm writes to pnpm-workspace.yaml now match the file's list indentation and dominant quote style #15571, #15079.
pnpm import now converts dependencies that use Yarn's patch: protocol. The dependency keeps the version it patches, and the patch file is added to patchedDependencies in pnpm-workspace.yaml. If the patch file is missing, pnpm prints a warning and imports the dependency without the patch #10278.
pnpm import in a workspace now keeps the versions pinned by the root yarn.lock, package-lock.json, or npm-shrinkwrap.json when another workspace project's range allows a newer version. Before, the root project got the newest version in its range #4385.
pnpm patch, pnpm patch-commit, and pnpm patch-remove now work in a project of a workspace with sharedWorkspaceLockfile: false. pnpm patch failed there with ERR_PNPM_PATCH_NO_LOCKFILE after a successful install. The reinstall after committing or removing a patch left the project's own node_modules unchanged #9926.
pnpm patch-commit now resolves default patch directory locations when passed a package name or package specifier (such as pnpm patch-commit <pkg> or pnpm patch-commit <pkg>@<version>).
pnpm patch-commit now updates the lockfile snapshot and prunes removed dependencies when the patch modifies package.json #6866.
pnpm patch-commit now falls back to copying package files when hard linking fails.
A script that pnpm runs without a terminal now ends when pnpm itself is killed. Killing pnpm's process group, as Playwright's webServer does to stop the command it started, used to leave the script running and holding the caller's output pipes open #15555.
pnpm --filter <project> <command> and pnpm -r <command> now run a command installed in the selected projects' dependencies when none of them has a script by that name. This matches pnpm <command> in a single project. pnpm run with --filter or -r still reports the missing script #10151.
pnpm exec and pnpm dlx now set npm_execpath, INIT_CWD, npm_node_execpath, and NODE in child environments when Node.js is available. Stale inherited NODE and npm_node_execpath variables are cleared when Node.js cannot be found on PATH #7037. Scripts that pnpx and pnx run now get pnpm itself as npm_execpath. A script that ran $npm_execpath install there ran pnpm dlx install.
pnpm exec now sets the PWD environment variable to the directory the command runs in. Shells and tools that read PWD now report the logical path of a workspace package reached through a symlink #1550.
A script that runs pnpm run no longer adds duplicate node_modules/.bin and node-gyp-bin entries to PATH #5352.
Concurrent pnpm run and pnpm exec commands now serialize their dependency installs #14551.
pnpm run and pnpm exec with verifyDepsBeforeRun now accept a moved project whose store is on the project's volume. Before, the check reported that the workspace structure had changed whenever the default store was not on the home volume.
verifyDepsBeforeRun checks now account for project-specific packageConfigs overrides in workspaces with sharedWorkspaceLockfile: false #15545.
pnpm restart now runs the "stop" and "start" scripts when the package has no "restart" script. Previously it ran "stop" and then failed with "Missing script: restart" #4750.
pnpm dlx now keeps a separate cache entry for each Node.js major version. A package built under one Node.js major version, such as a native addon, is no longer reused under another #8611.
pnpm pipeline no longer fails when run in a project outside a Git work tree or on a system without git. Tasks in those projects run without caching, and pnpm prints a warning explaining why #15601.
A runtime: version range that contains || or a space, such as a devEngines.runtime version of ^22.18.0 || ^24.0.0, now installs the requested runtime. pnpm used to install the npm package with the same name, such as node #14817.
When the configured scriptShell does not exist, running a script now fails with an error that names the shell. Previously pnpm printed only an exit code or the package directory #7562.
A script killed by a signal now fails with an error that names the signal, such as Command failed with signal SIGKILL. #9821.
pnpm publish now resolves workspace: dependencies from workspace manifests when node_modules is not installed. Previously, publishing without node_modules failed with ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL #6567.
pnpm publish now honors publishConfig["@scope:registry"] for a package in that scope. It takes precedence over the registry set for the same scope in .npmrc and over publishConfig.registry #12071.
pnpm pack and pnpm publish now include bundled dependencies when using the isolated linker. This covers workspace packages and the dependencies of each bundled package. Bundled dependencies are also included when publishConfig.directory selects a build directory #1643.
pnpm pack, pnpm deploy, and installs of local directory dependencies now keep symlinks that point to files or directories included in the package. pnpm pack leaves out symlinks that point outside the package #8208.
pnpm pack now preserves file executable permissions in the packed tarball when source files are executable on disk.
pnpm publish and pnpm pack now report a missing version or name field on a workspace dependency. Previously, pnpm reported that the dependency was not installed #4164.
pnpm publish and pnpm pack now report an error when a bin script has a shebang line ending with CRLF #7311.
pnpm deploy now copies the packageManager and devEngines.packageManager fields of the workspace root package.json into the deployed package.json, unless the deployed project pins a package manager itself #9079.
pnpm deploy now puts the virtual store at virtualStoreDir, resolved against the deploy directory. A shared-lockfile deploy records virtualStoreDir in the deployed pnpm-workspace.yaml. With the global virtual store enabled or an absolute virtualStoreDir, the deploy still uses node_modules/.pnpm #8787.
pnpm deploy now respects --package-import-method passed on the command line and reports the package import method correctly #7593.
pnpm deploy does not run the prepare scripts of the deployed project #7282.
pnpm deploy --legacy no longer rewrites the source workspace's node_modules/.pnpm-workspace-state-v1.json to describe only the deployed project #15352.
pnpm now reads and updates package.json5 project manifests. Manifest updates retain comments, and workspace discovery prefers package.json, then package.json5, then package.yaml #15129. pnpm pack includes exactly one package.json in the archive when the project uses an alternative manifest format, even when .npmignore or files excludes the source file.
Git-hosted dependencies that use a package.yaml or package.json5 manifest now honor its files field #7906.
Fixed pnpm version failing on projects using a package.yaml manifest.
Fixed pnpm init creating an extra package.json when package.yaml is already present.
pnpm version now applies pending bumps to private workspace packages. A private package's changelog is written to its committed CHANGELOG.md, also when versioning.changelog.storage is registry #13736, #13519.
pnpm init now supports the --bare option. It creates a package.json file with only the required fields #15538.
The reporter setting is now honored when it is configured in pnpm-workspace.yaml, the global configuration, or the PNPM_CONFIG_REPORTER environment variable. Configured reporter: silent makes silent output the default. An explicit --reporter takes precedence #4879.
.npmrc and pnpm-workspace.yaml files now support npm's ${VAR?} placeholder. It expands to the value of VAR, or to an empty string without a warning when VAR is unset #14404.
pnpm now expands environment variables in _auth.authToken values loaded from global config.yaml and pnpm_config__auth.
pnpm now keeps the configured default registry when _auth holds credentials for several registries and some of those registries serve package scopes.
Lockfile verification checks a tarball hosted on a scoped registry against that registry's metadata, unless the package's own scope has a registry assigned #15530.
pnpm now parses the first setting in a .npmrc that starts with a UTF-8 byte order mark. Previously, the leading byte order mark caused the first line's key to be ignored #15353.
pnpm now prints a warning when a .npmrc, auth.ini, or the file set by npmrcAuthFile exists but cannot be read. The settings in such a file were ignored without any message. A .npmrc that contains invalid UTF-8 is now read #5065.
pnpm config set and pnpm config delete now preserve comments and repeated keys such as ca= in .npmrc #14851.
pnpm login now logs back in to an existing user on registries without web login, such as verdaccio. The classic login request sends the username and password as basic auth, as npm login does #12055.
Commands that do not use the store no longer create a temporary file in the project directory when they load their settings. These include pnpm view, pnpm config, pnpm root, pnpm bin, pnpm exec, pnpm run, the script shortcuts such as pnpm test, and the registry commands such as pnpm whoami, pnpm dist-tag, and pnpm search. pnpm exec, pnpm run, and the script shortcuts still create one in projects that declare configDependencies.
Global commands such as pnpm add --global, pnpm list --global, and pnpm bin --global now run with the pnpm you invoked, even in a project that pins another pnpm version. Previously, a pin with onFail: "download" switched them to the pinned pnpm, and a pinned pnpm 10 or older failed because its global bin directory was not in PATH #14531.
pnpm add -g, pnpm update -g, and pnpm remove -g no longer fail with ERR_PNPM_PACKAGE_MANIFEST_IO_ERROR when another global package's link into the store dangles, for example after the store was pruned. The pnpm install script failed the same way on such a machine.
pnpm update --global now skips a global package installed from a file: path that no longer exists, prints a warning, and updates the remaining global packages. Previously the whole update failed with ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND #12533.
pnpm self-update run in a project that pins pnpm through packageManager or devEngines.packageManager now also updates the global pnpm, as it does outside a project #14747.
pnpm self-update no longer leaves the previous pnpm in the global packages when it was installed as @pnpm/exe. pnpm ls -g now lists a single pnpm #14709. pnpm setup now installs pnpm under the package name pnpm too, so both commands leave the same shims in the global bin directory. After a self-update on Windows, PowerShell ran pnpm through pnpm.cmd and asked "Terminate batch job (Y/N)?" on Ctrl+C #15567.
pnpm setup failed with Text file busy (os error 26) when $PNPM_HOME/bin already held pn, pnpx, or pnx as links to the running pnpm executable. It now replaces those files and completes #15494.
pnpm setup no longer deletes aliases and other lines that sit between a # pnpm comment and the pnpm block in a shell startup file #7067.
When pnpm switches to the version a project pins, the minimumReleaseAge approvals for that version are now added to minimumReleaseAgeExclude in the project's pnpm-workspace.yaml. A project without that file gets one. Global commands leave the project's settings unchanged #15396.
pnpm env remove now cleans up dangling Node.js executables and shims. Surviving global commands remain intact.
Node.js runtime resolution now supports Windows ARM64. Node.js 20 and newer resolve native win-arm64 builds, and older versions fall back to win-x64 under emulation #7123.
On Windows, pnpm clean and installs no longer fail immediately when another process uses a package in node_modules. pnpm waits up to a minute for an open file. It waits up to 5 seconds for a running program #15081.
pnpm install in WSL now waits out Windows file locks on a Windows drive such as /mnt/c, as it already does on Windows. Before, an antivirus or indexer scan holding a file open could fail the install with EACCES #6155.
On Windows, pnpm now retries saving pnpm-lock.yaml for up to a minute while another process holds the file open. The save used to fail at once with EPERM, EBUSY, or "Access is denied" #9461.
pnpm now escapes trailing dots and spaces in node_modules/.pnpm directory names. Windows strips these characters, so a dependency such as "parent-pkg": "file:../" created a directory that could not be deleted or failed to install #8101.
On Windows, bin shims run from Git Bash, MSYS2, or Cygwin now pass NODE_PATH to Node.js as Windows paths. A project installed from cmd or PowerShell gave its bins a NODE_PATH under the Git install directory when they ran from Git Bash. Installing again replaces the shims already in node_modules #3360.
Fixed scripts failing with errors such as 'an-compile' is not recognized when scriptShell is set to cmd.exe on Windows #7181.
On Windows, the error for a node_modules directory that pnpm cannot move out of the way now names the directory and says that a file in it is probably in use by another process #7505.
pnpm audit and pnpm audit signatures now check only the dependencies of the projects selected by --filter, --filter-prod, or --workspace-root. The filter used to be ignored, so a filtered audit reported the whole workspace #10982.
pnpm audit now lists at least one dependency path from every workspace project that depends on a vulnerable package. Before, a project whose dependency was reached through more than 100 paths filled the path list, and other projects that depend on the same package were left out #12200.
pnpm audit --fix now prunes redundant overrides when one vulnerable range is a subset of another for the same package #8577.
Running pnpm list inside a workspace package without --recursive or a filter now lists only the current package #14494. pnpm licenses list does the same. Use --recursive or --filter to list the licenses of other workspace projects #5689.
pnpm list --only-projects now prints every project selected with --filter or --recursive, including a project that has no workspace dependencies #9770. It also lists the workspace projects when sharedWorkspaceLockfile is false #7151, and a project that sets publishConfig.directory #10635. It no longer reports packages in node_modules that are missing from the lockfile #9528.
pnpm licenses list failed or reported nothing in a workspace with sharedWorkspaceLockfile: false. It now reads the lockfile of each selected project #10140.
With nodeLinker: hoisted, pnpm licenses list reported every license as Unknown and listed paths under node_modules/.pnpm that do not exist. It now reads each package from the directory where the hoisted linker placed it #8589.
pnpm outdated and pnpm -r outdated now fail with ERR_PNPM_NO_PACKAGE_IN_DEPENDENCIES when a requested package selector does not match any dependency in the inspected projects #2319.
pnpm -r outdated --json now includes every outdated workspace dependency when multiple projects depend on different versions or dependency types of the same package. Such a package is keyed by its current version and dependency type, for example vue@2.7.14 (dev) #7693.
pnpm sbom filtered to a single workspace project now takes the author, description, license, repository, and bugs fields from the workspace root package.json when the project does not declare them. A field the project declares is never taken from the root, even when it is blank or null #14882.
pnpm store status no longer reports a package as modified when it has build or postinstall scripts, peer dependencies, or skipped optional dependencies #15383. When packages were mutated, it now lists only those packages and no longer suggests running pnpm install --force #919.
pnpm peers check and the ERR_PNPM_PEER_DEP_ISSUES error now group peer dependency issues under the workspace project they were found in #15351.
The error for an incompatible pnpm-lock.yaml now reports the lockfileVersion the file was generated with and the lockfileVersion the current pnpm supports #848.
When the registry stops sending data for longer than fetchTimeout, pnpm now reports that the metadata or tarball request timed out. Previously the error did not mention the timeout #3646.
Fatal peer dependency errors and their hints are now written to stderr #5419.
pnpm run with --loglevel set to warn, error, or silent (or the same loglevel setting) no longer prints the $ <command> line before a script, nor the summary of the install that verifyDepsBeforeRun runs first. Both are info-level output #8944.
pnpm run and pnpm exec now print No projects matched the filters in "<workspace>" when --filter selects no project #8408.
pnpm dedupe now counts each package reused from the store once in its progress output #15303.
pnpm add now warns when replacing an existing dependency with a specifier pointing to a different source #14869.
pnpm link now warns when linking a package that declares one or more peer dependencies, explaining that the linked dependency will not resolve peer dependencies from the target node_modules and suggesting the file: protocol instead.
pnpm import now warns when package.json lists projects in a "workspaces" array and there is no "pnpm-workspace.yaml". Without that file, the import writes a lockfile for the root project only #5240.
Bash completion now completes script names that contain a colon, such as pnpm run test:u to pnpm run test:unit #5482.
pnpm install warnings no longer carry the text of a package's deprecation notice, naming only the deprecated package and version #15099. A deprecation…
pnpm 12.6.0 ships with automatic dependency deduplication, relocatable node_modules, package.yaml manifest editing, and --save-types support.
autoDedupe deduplicates compatible dependency versions during installation #7258. Enable it in pnpm-workspace.yaml or use pnpm install --auto-dedupe or pnpm add --auto-dedupe. Frozen installs leave the lockfile unchanged.
pnpm install, pnpm run, and pnpm exec on macOS and Linux now reuse a node_modules directory and bin shims that moved or were copied together with their project #6937. The first command after the move checks the tree and records its new location, so project commands in node_modules/.bin keep working.
pnpm add --save-types saves available @types/* packages in devDependencies alongside registry dependencies #3868. Packages that declare bundled TypeScript types are skipped. Set saveTypes: true in pnpm-workspace.yaml to enable this by default.
package.yaml manifests can now be updated by pnpm add, pnpm update, pnpm remove, pnpm pkg, pnpm link, pnpm set-script, and pnpm version #2008. Existing comments and key order are preserved.
Catalog entries can now use the file: and link: protocols #8642. A relative path or bare path in an entry, such as ./tarballs/foo.tgz, is measured from the directory holding pnpm-workspace.yaml.
pnpm tasks status lists running and waiting tasks in each concurrency group, and waiting tasks now take available slots in arrival order with higher priority tasks going first #15208. If workspaces use different limits for the same group, a later task can take a free slot that earlier tasks cannot use. A package script named tasks takes precedence; use pnpm pm tasks status when that script exists.
pnpm cache prune deletes registry metadata cache directories that this version of pnpm can no longer read #15046. pnpm cache prune --dry-run lists what it would delete without removing anything.
macosBackup.excludeModulesDir and macosBackup.excludeStoreDir on macOS can now exclude newly created modules, virtual-store, and package-store directories from Time Machine #6440. Set either to true in global configuration or using the PNPM_CONFIG_MACOS_BACKUP_EXCLUDE_MODULES_DIR and PNPM_CONFIG_MACOS_BACKUP_EXCLUDE_STORE_DIR environment variables.
pnpm add --tilde is now an alias for --save-prefix=~ #12863. The Yarn -T shorthand is not supported.
progress setting and --no-progress option now turn off dependency and download progress lines #14065. Warnings, lifecycle output, and the dependency summary are still printed.
POSIX bin shims now take cygpath and wslpath from the system default path on Cygwin, MSYS2, and WSL2 so a dependency cannot redirect another package's shim #14866.
pnpm install warnings no longer carry the text of a package's deprecation notice, naming only the deprecated package and version #15099. A deprecation warning names the newest non-deprecated version when one exists, and control characters and line separators are stripped from package identifiers and warnings.
pnpm install and other commands that report configuration warnings now warn when environment variables in project .npmrc credentials are ignored #15051.
pnpm install --frozen-lockfile now succeeds when an optional dependency was unresolvable and skipped by the install that wrote the lockfile #3960.
pnpm install --frozen-lockfile no longer installs dependencies of projects removed from pnpm-workspace.yaml #15248. Missing local tarballs used only by those projects no longer fail the install.
pnpm ci now empties node_modules before installing in a project that declares a clean script #15276.
pnpm install --force now re-imports every package into the virtual store #15030 and removes obsolete dependency links inside virtual-store packages when their dependencies change #15039.
preinstall script for the root project now runs before dependencies are resolved and linked #3760.
pnpm install now runs pnpm:devPreinstall when the root project uses package.yaml #15168.
pnpm install now enforces the root project's engines.node range when engineStrict is enabled #3016.
pnpm install now uses the running Node.js when devEngines.runtime declares a range without onFail: download #15230.
pnpm install no longer hangs when a git dependency is fetched over SSH and ssh prompts for a passphrase or host key confirmation, running ssh in batch mode instead #2227.
pnpm install now installs git-hosted dependencies without preparing them when their builds are explicitly denied by allowBuilds #10522.
pnpm install now reuses an in-flight tarball download when another resolution of the same archive still needs its package.json #15037.
pnpm install --prod no longer downloads registry packages that only a devDependency reaches #881.
pnpm install --no-runtime --frozen-lockfile with nodeLinker: hoisted no longer fails on repeated runs with a broken lockfile #15212.
pnpm install and pnpm update now resolve a dependency range to the newest matching version that is not deprecated #15128.
pnpm add <pkg> without a version now uses the catalog entry when the workspace already catalogs that package #14865.
pnpm install now links workspace dependencies declared with plain version ranges when excludeLinksFromLockfile and linkWorkspacePackages are enabled #15133.
pnpm install now resolves local tarball dependencies whose absolute file: paths contain .. consistently and skips reinstallation on repeat installs #15190.
pnpm install now installs dependencies when a custom resolver returns a local or git-hosted tarball without a manifest #15016.
pnpm.overrides entries written as a bare path, such as ./local-dep, are now measured from the directory holding pnpm-workspace.yaml #11131.
pnpm update --no-save no longer bypasses version-scoped overrides when a dependency selector specifies a version #14923.
pnpm peers check and strict peer dependency checks no longer reject compatible versions from named registries #15225.
pnpm outdated and pnpm update --interactive --latest now include named-registry dependencies such as work:2.1.0 and preserve their registry prefix #15226.
Workspace projects selected by hoistPattern or publicHoistPattern are now hoisted on every install #3642.
Workspace packages with SemVer build metadata are no longer skipped when they match the requested range and have the same version precedence as the registry package #2812.
Sped up pnpm dedupe and pnpm install in projects with many convergence overrides by checking overrides concurrently #15175.
minimumReleaseAge is no longer skipped for packages served by registries returning matching ETags for abbreviated and full package metadata #14925.
pnpm run signal handling no longer delivers a redundant second SIGINT to child scripts on Ctrl+C in a terminal, and properly forwards termination signals when running non-interactively without a terminal #7374.
pnpm run and pnpm exec in workspaces with sharedWorkspaceLockfile: false now verify dependencies in the selected projects rather than expecting a root workspace state #15272.
pnpm test now forwards --filter arguments to the test script when the option follows the shortcut #15217.
Recursive runs now start scripts matched by a /pattern/ selector in parallel within workspaceConcurrency #14933.
pnpm deploy, pnpm rebuild, pnpm rb, and pnpm setup now prefer a package.json script of the same name #14976.
modulesDir custom directory names now support executable lookup and CommonJS plugin resolution across pnpm run, pnpm exec, pnpm version hooks, and lifecycle scripts #3604.
pnpm install-test now accepts --no-bail directly and in recursive runs #3777.
pnpm commands run in a project not included in the workspace now act on that project alone #3561.
pnpm-workspace.yaml edits now preserve scalar YAML anchors and aliases #8245.
pnpm-workspace.yaml now expands environment variable placeholders with fallback syntax in enum-valued settings such as nodeLinker #14914.
pnpmfile configuration now loads a .js file as CommonJS or an ES module, following the nearest package.json #15141.
updateConfig hook settings are now honored by pnpm peers check, why, list, ll, licenses, audit, sbom, fetch, patch, patch-commit, patch-remove, approve-builds, and runtime #15047, #15049.
readPackage hook changes or removal now take added dependencies out of pnpm-lock.yaml and update dependencies when an existing lockfile is present #3735, #15136.
package.yaml projects now record their pinned pnpm under packageManagerDependencies in pnpm-lock.yaml #15167.
packageManagerDependencies pinning @pnpm/exe beside pnpm is no longer rewritten in pnpm-lock.yaml #14926.
pnpm now preserves CRLF line endings when modifying project manifests #3529.
loglevel setting is now honored when configured in pnpm-workspace.yaml, global configuration, or PNPM_CONFIG_LOGLEVEL #3122.
storeDir values loaded from global configuration or PNPM_CONFIG_STORE_DIR now expand a leading ~/ to the user's home directory #6560.
--shared-workspace-lockfile now produces a warning when passed on the command line outside a workspace #1617.
pnpm install on Windows now runs dependency build scripts from long global virtual store paths and normalizes scoped package paths in lifecycle script PATH entries #15111.
pnpm install across projects sharing a global virtual store on Windows no longer fails with Access is denied, file-exists errors, or transient sharing violations #15114, #15176, #15171.
pn, pnpx, pnx, and pnpm now run when Git Bash, MSYS2, or Cygwin launches them through a Windows path #14884.
pnpm dlx now reuses cached packages when Windows creates directory junctions for its cache links #15171.
pnpm pipeline --watch now resolves Windows short paths so multiple path representations share the build cache #15105.
pnpm remove now runs the project's own preuninstall, uninstall, and postuninstall scripts #3276.
pnpm remove -r now fails before modifying manifests if any requested dependency is absent from all selected projects #2319.
pnpm update --peer now updates ranges in peerDependencies #8081.
pnpm update now moves devEngines.runtime and engines.runtime version ranges to the resolved Node.js version #14988.
pnpm update -g no longer reinstalls unchanged packages #12002.
pnpm add -g, pnpm update -g, and pnpm remove -g now recover a global package group whose node_modules directory was deleted #15093.
pnpm add -g now installs local tarballs when PNPM_HOME contains .. path segments #15118.
pnpm version now reads tagVersionPrefix from pnpm-workspace.yaml, global config, or PNPM_CONFIG_TAG_VERSION_PREFIX when creating and reading Git tags #15044.
pnpm publish now allows a detached Git HEAD in CI environments #5894.
pnpm store prune now removes unreferenced files and packages from the content-addressable store #3635, as well as expired or superseded pnpm dlx cache data #15171.
pnpm cache list-registries now prints decoded registry URLs #15046.
pnpm deploy no longer triggers an install when running scripts in a read-only deployed filesystem #11617.
pnpm -r list --json now outputs a single JSON array when sharedWorkspaceLockfile is false, and --long and --parseable read each project's own modules directory #15011.
pnpm sbom now validates SPDX identifiers and expressions before emitting them as CycloneDX license IDs or expressions, falling back to a license name for non-SPDX values such as UNLICENSED #14786.
pnpm change check now validates pending change intents in .changeset/ #15183.
pnpm --filter and pnpm -F shell completion now suggests workspace package names #15216. Completion candidates containing control or invisible formatting characters are omitted so package and script names cannot inject terminal escape sequences.
pnpm run and pnpm run-script shell completion now suggests package scripts #15034.
pnpm --version no longer creates a temporary file in the project directory during store detection #15264.
pnpm setup now describes displayed configuration changes as "The following configuration changes were made" #15100.
minimumReleaseAge approval prompts in pnpm install and pnpm update -g now count and display each package version once #15083, #15091.
.npmrc authentication warnings now report when an empty environment variable removes an auth token and name the affected key #4806.
The install summary now names the version each dependency resolved to when node-linker is hoisted #15161.
pnpm install now re-links a package's global virtual store slot after allowBuilds changes #15117.
pnpm now reports an unknown task setting in pnpm-workspace.yaml and carries on. It used to refuse to start, so a project could not use a task setting
pnpm now reports an unknown task setting in pnpm-workspace.yaml and carries on. It used to refuse to start, so a project could not use a task setting that only the pnpm version its packageManager pins reads. The setting is still an error when the running pnpm is that pinned version.
Python interpreter installation now retries historical release metadata requests. It caches the release list for up to 24 hours and refreshes it once after a lookup miss. When a release omits the current platform, the search samples at most eight other releases before reporting that the lookup is inconclusive.
Python registries entries now route packages by exact names or trailing-prefix patterns in packages. Registry declaration order no longer affects resolution. A matched package resolves exclusively from its assigned registry, including transitive and build dependencies. Use packages: ["*"] to declare the default index.
pnpm install no longer fails with "Too many levels of symbolic links" when a Cargo configuration file above the workspace is a symlink, such as a ~/.cargo/config.toml linked from a dotfiles repository.
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.
pnpm install no longer refuses the repeat-install fast path just because a changed pnpm-lock.yaml is 16 MiB or larger. Such a lockfile forced a full install on the run after every change.
pnpm 12.5.0 makes Python a first-class ecosystem, accepts Package URLs in pnpm add, names whole platforms in supportedArchitectures, and gives tasks m
pnpm 12.5.0 makes Python a first-class ecosystem, accepts Package URLs in pnpm add, names whole platforms in supportedArchitectures, and gives tasks machine-wide concurrency limits. It also fixes an install that could reuse one package's downloaded tarball for another.
pnpm add accepts a Package URL in place of a package name. pnpm add pkg:npm/express@4.18.2 saves express to package.json. pnpm add pkg:cargo/serde@1.0.188 saves serde to Cargo.toml. pnpm add pkg:pypi/requests@2.31.0 saves requests to pyproject.toml. pkg is now a reserved specifier prefix, whatever case it is written in, so a named registry can no longer be called pkg.
A registries entry can now name the ecosystem it serves.
registries:
https://internal.example/simple/:
ecosystem: pypi
https://pypi.org/simple/:
ecosystem: pypi
https://index.crates.io/:
ecosystem: cargo
ecosystem accepts npm, cargo and pypi. An entry that does not name one serves npm, as every entry did before.
An ecosystem with several indexes searches them in the order they are declared. The first index that has a package supplies it, so the one declared last answers what none before it had.
A registries entry may not carry credentials. pnpm reads them from .npmrc, matched by origin, for a PyPI index as for every other package source.
supportedArchitectures now accepts a list of platforms, in place of the os, cpu and libc axes.
supportedArchitectures:
- linux-x64
- darwin-arm64
- win32-x64
An install prepares for the platforms the list names, and for those only. A platform reads as <os>-<cpu>, with a C library on Linux, as in linux-x64-musl or linux-x64-manylinux_2_28. The Rust target triple of the same machine is accepted too, so x86_64-unknown-linux-gnu names the platform linux-x64 names. A Linux platform that names no C library is the glibc platform. current is the platform the install runs on.
The os, cpu and libc mapping keeps working and keeps its meaning.
Added concurrency groups for tasks. A task in pnpm-workspace.yaml can name a concurrencyGroup. The new concurrencyGroups setting gives each group a limit. At most that many tasks of the group run at once on the machine, counted across every pnpm process, pnpm pipeline included. A task past the limit waits for a running one to finish. A script that calls pnpm run for a task of the same group runs under the slot its parent holds.
tasks:
test:rust:
concurrencyGroup: cargo
concurrencyGroups:
cargo: 2
tools names the programs pnpm downloads, and mirror says where each one comes from.
tools:
node:
mirror: https://mirror.example.com/node/download
channels:
nightly: https://nightly.example.com/
bun:
mirror: https://mirror.example.com/bun
python:
mirror: https://mirror.example.com/python-build-standalone/releases
node, bun and python can be named. Any other tool is refused.
mirror is the base a tool's own layout hangs off.
channels sends one release channel elsewhere. A channel neither it nor node-mirror:<channel> names is left to mirror. Only node publishes channels, so naming them for another tool is refused.
Set it in the global config.yaml or in PNPM_CONFIG_TOOLS. A pnpm-workspace.yaml that names a tool mirror is ignored.
pnpm pack-app downloads the Node.js it embeds through tools.node. node-mirror:<channel> keeps working and names the same thing as an entry under channels.
pnpm install now chooses a Python interpreter for each project instead of installing every project with one interpreter #14945. A project is installed with the first interpreter on the machine that its requires-python accepts, so a workspace can hold projects that support different Python versions. pnpm reads .python-version too, and prefers the version it asks for. Set python.executable in pnpm-workspace.yaml to name one interpreter for every project.
pnpm install now installs a Python interpreter when no interpreter on the machine fits the project #14945. The builds are python-build-standalone's, which uv and rye install too. One interpreter is shared by every project on the machine, and a later install uses it without downloading anything. runtimeOnFail decides what an install with no interpreter that fits does, the way it does for a Node.js runtime. error reports the project instead of installing one. warn and ignore install with an interpreter the machine has that the project's requires-python rejects. tools.python.mirror names a mirror.
Python environments now live in the store. Each project keeps only its .venv link, which points at the project's current environment generation under python-envs in the store. A repository with many Python projects no longer holds a .pnpm/python-envs directory in each of them. The next install relinks a .venv that an earlier release published. The old .pnpm/python-envs directory is left in place, since a running program may still use it, and can be deleted once none does. With frozenStore set, pnpm writes nothing to the store, so environments stay in the project's .pnpm/python-envs #15014.
Python environments now use packageImportMethod to import wheel files from the store. Use clone-or-copy for copy-on-write clones with a copy fallback, or copy for independent files. Hardlinked files share writes with the store and other environments.
Isolated Python build environments keep backend writes private with copy-on-write clones or copies.
pnpm install now installs a Python project's own package, so the project can be imported and the commands in [project.scripts] run right after an install #14945. The installed package points at the source tree, so an edit to a module takes effect without another install. pnpm installs the package of a project that declares a [build-system]. tool.uv.package overrides that either way.
pnpm install now installs a Python project in the workspace from its own source. Declare it under [tool.uv.sources], as shared = { workspace = true } or shared = { path = "../shared", editable = true }. pnpm builds the project with the backend it declares. It installs the build editable, so an edit to the project takes effect without another install.
Approve the build backend under allowBuilds in pnpm-workspace.yaml as a Package URL, as pkg:pypi/hatchling: true. An install that has not approved a backend does not build the projects that need it. The message names the key to add.
pnpm install now refuses a requirement that names a project in the workspace when nothing declares where it comes from. It used to take that name from the index.
The members of a uv workspace can now share one Python environment. Set shared-environment = true under [tool.pnpm.python] in the pyproject.toml that declares [tool.uv.workspace]. pnpm install then resolves every member as one graph into one pylock.toml and one .venv at the workspace root. Two members that require versions of a distribution no release satisfies at once are refused with an error naming both. Each project still gets an environment of its own by default #15015.
Python projects can now select extras and dependency groups through [tool.pnpm.python] in pyproject.toml #14945. Workspace python.extras and python.groups defaults now skip names a project does not define.
pnpm install now reads dynamic Python project metadata from the build backend #14945. Projects with only a requirements.txt file now get a Python environment and lockfile.
pnpm can now resolve pylock.toml for several platforms and Python versions at once. supportedArchitectures names the platforms to lock for and python.versions the versions. Every platform is paired with every version. One committed lockfile then serves Linux CI and macOS or Windows contributors #14945.
supportedArchitectures:
- linux-x64-manylinux_2_28
- darwin-arm64
- win32-x64
python:
enabled: true
versions: ['3.12', '3.13']
The lockfile pins the wheel each environment takes for a distribution. It marks a package only some environments install. pnpm install takes the packages and wheels of the environment its interpreter matches, and refuses an interpreter none of them stand for. pnpm resolves a project that declares environments itself, not through the server pnprServer names. Naming neither setting locks for the interpreter running the install.
python.overrides and python.constraints pin the versions a Python resolution may pick #14945. pnpm reads uv's own overrides and constraints from pyproject.toml too.
pnpm install now supports Python dependencies from Git repositories #14945. Direct wheel URLs are also supported. Sources can be declared in [tool.uv.sources]. Git dependencies require allowBuilds approval.
pnpm install can install a Python release that publishes no wheel this interpreter accepts, by building the source distribution the index serves beside it #14945. The archive is pinned in pylock.toml by name and SHA-256. A later install replays it from the store, offline included. Building a source distribution runs the release's own build backend. Approve it with pkg:pypi/<distribution>: true under allowBuilds.
A resolution that finds no version of a distribution now says why. It tells apart a distribution no index publishes, one whose releases publish nothing this interpreter can install, and one whose versions the project's requirements exclude.
pnpm no longer reuses one package's downloaded tarball for another package whose resolution pins a different integrity hash to the same URL #15021.
pnpm install and pnpm add now report an error when package.json, pnpm-lock.yaml, pyproject.toml or another file they snapshot before installing is a named pipe or a device. The command used to wait forever for something to write to it.
pnpm install --prod and pnpm install --dev now record every dependency group in pnpm-lock.yaml. node_modules still holds only the groups the filter selects. They used to write the filter into the lockfile, so a later pnpm install --frozen-lockfile rejected it. pnpm prune --prod, pnpm prune --dev, and pnpm prune --no-optional behave the same way #14912.
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.
Two pnpm processes installing one workspace at the same time no longer fail on Windows with "Access is denied" while writing node_modules/.pnpm-workspace-state-v1.json. The write now retries the transient lock the other process holds, as pnpm's other file writes do.
pnpm now reads the manifest from the tarball when a pnpmfile resolvers hook returns a resolution without one. Such a package installed alone, with none of its own dependencies and no warning #15000.
pnpm install now merges Git conflict markers in pnpm-lock.yaml. It parses both sides of the conflict and keeps the versions they locked. A conflict in the config dependencies recorded at the top of the lockfile is merged too #14880.
pnpm install can now generate Cargo.lock for workspaces with path or Git [patch] and [replace] overrides. Adding, removing, and updating crates also preserve these overrides #14950.
Cargo lockfile resolution blocks unsupported Git transport helpers declared by transitive dependencies.
pnpm install now vendors recursive Git submodules for Cargo dependencies at their pinned commits. Cargo builds can use these sources offline. Set Git's protocol.file.allow to always to fetch local file submodules. pnpm fetches cached Git crates again on the first online install #14951.
pnpm install now generates Cargo.lock for workspaces with Git dependencies, including a dependency that omits a package version. It also downloads the Rust standard library's dependencies when Cargo configuration enables build-std #14944.
pnpm install now handles weak Cargo features, written crate?/feature. Resolution failed when one dependency turned on an optional crate and another asked for a weak feature of it #14960. The generated Cargo.lock now also includes the dependencies weak features reference, which Cargo rejected with --locked for crates such as uuid #14978.
pnpm install now generates Cargo.lock when a crate version it considers depends on a release the registry carries only as yanked. pnpm rules that version out and resolves the rest of the graph. Resolution failed with an error such as no non-yanked version of napi-build satisfies ^3.0.0-beta #14952.
pnpm install now falls back to an older semver-incompatible version of a crate when the newest one a dependency range allows cannot be resolved. Ranges such as >=1, <3 span several of them #14962.
pnpm install now honors uv workspace members when discovering Python projects. When no uv workspace declares a project, pnpm skips projects under conventional example, demo, documentation, template, test, tests, and test fixture directories #15058.
pnpm install --filter <selector> now installs only the Python projects the selection asks for. A Python project that shares a directory with an npm workspace project is selected with that project. A Python project in a directory of its own is selected by the distribution it declares, by its path, or through the [tool.uv.sources] entries that reach it. Under --fail-if-no-match, a selector that names only a Python project is a match. pnpm add --filter <selector> pypi:<package> writes the requirement to every selected project #14945.
pnpm install now installs wheels whose RECORD hashes disagree with their contents. The wheel archive's locked SHA-256 hash remains verified. pnpm writes correct hashes to the installed RECORD #15061.
pnpm install now installs a Python wheel whose WHEEL file lists tags that differ from the ones in its filename. A wheel whose filename tags were changed after the build, such as mysql-connector-python, was rejected #14945.
A Python release whose wheel metadata declares a requirement pnpm cannot read no longer fails the install. pnpm now resolves the project against the other releases of that package, and reports the unreadable requirement when none of them works.
pnpm install no longer fails when a Python index lists a file pnpm cannot use, such as a release with no SHA-256 digest or an unreadable wheel filename. That file is left out and the project resolves against the remaining releases.
pnpm add pypi:<package> in a directory that has no pyproject.toml now names the missing file and says where to run the command. It used to fail with a bare No such file or directory (os error 2) #14945.
pnpm audit no longer hangs on dependency graphs with many shared dependencies #15005.
Sped up pnpm install in Python workspaces with many projects. Projects now prepare concurrently. Projects with identical registry requirements also share fresh dependency resolutions #14945.
Repeat installs through the Node-API bindings now return "Already up to date" when the project manifests still match pnpm-lock.yaml. Before, every such install reinstalled the whole tree. An install also no longer reinstalls when pnpm-lock.yaml differs from the installed dependencies only by packages no project depends on or by top-level keys pnpm does not define.
pnpm deploy now links commands exposed by workspace dependencies into the deployed project's node_modules/.bin directory #14899.
pnpm dlx and pnx now prompt to approve dependency build scripts in interactive terminals #14943. Cached packages with pending builds also prompt for approval. Without an interactive terminal, use --allow-build to allow the required builds.
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.
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.
pnpm outdated --long fills the Details column with the package homepage again #14886.
pnpm 12.4.2 includes security fixes for executable shims and GitHub Actions links, more reliable installs, faster peer dependency checks in workspaces…
pnpm 12.4.2 includes security fixes for executable shims and GitHub Actions links, more reliable installs, faster peer dependency checks in workspaces, and Python lockfiles that work across compatible targets.
Dependency executables can no longer take over another package's POSIX bin shim through its shell helpers. Reinstall dependencies to replace existing shims #14837.
On Cygwin, MSYS2, and WSL, shims still use PATH for Windows path conversion, so dependency executables can still redirect them there.
GitHub Actions homepage links no longer expose server credentials. GitHub server URLs now require HTTPS, with HTTP allowed only for loopback hosts.
pnpm no longer crashes at startup on FreeBSD and other Unix-like platforms. Platforms other than Windows and macOS use ~/.local/share/pnpm/store by default #14859.
pnpm install on Windows no longer fails with ERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIR when clearing node_modules containing linked dependencies, such as when changing nodeLinker #14790.
pnpm install <pkg> now accepts --prod and --dev, including --prod=false #14868.
pnpm install and pnpm update now honor --ignore-workspace in nested projects excluded from the surrounding workspace. The flag also skips that workspace's settings during the packageManager check #14809.
pnpm install on macOS no longer reuses stale files for file: tarball or git-hosted tarball dependencies.
pnpm install in a single-project directory now detects package.json edits made while the previous install was finishing #14890.
pnpm install --frozen-lockfile now removes packages no longer reachable from any project in pnpm-lock.yaml. This also prevents repeated lifecycle script execution and unnecessary installs before pnpm run and pnpm exec with verifyDepsBeforeRun #14891.
Node.js runtime resolution now reports network failures from unofficial-builds.nodejs.org. These failures previously omitted musl builds from pnpm-lock.yaml, making its contents depend on network access #14813.
pnpm install now rejects invalid peerDependencies specifiers with ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION. A value such as "foo": "foo@1.0.0" previously created a broken directory link #14791.
pnpm deploy now writes plain registry versions in the deployed package.json, without peer dependency suffixes. The lockfile retains peer bindings, and npm aliases retain their target package names #14873.
pnpm add <git repository> now names repositories without a package.json as @owner/repo, allowing dependencies on equally named repositories from different owners #14870.
Peer dependency resolution now deduplicates packages whose child dependency resolves an optional peer in only some workspace projects, such as next with styled-jsx's optional babel-plugin-macros peer #14800.
pnpm update now settles the lockfile in one run when an upgrade removes the package providing an optional peer dependency #14895.
pnpm update --no-save now preserves override-applied specifiers for dependencies it is not updating, preventing subsequent frozen installs from failing with ERR_PNPM_OUTDATED_LOCKFILE #14836.
pnpm update --no-save now succeeds under minimumReleaseAgeStrict when every resolved version is old enough #14835.
Workspace installs and pnpm peers check are faster when projects depend on each other, fixing a slowdown introduced in 12.3.0. Unmet peer dependencies of workspace packages are now reported only under projects that link them directly #14906.
Hoisted installs use less memory when packages are cached. Frozen-lockfile hoisted installs on macOS are also faster when reusable package directories are cached.
pnpm install --frozen-lockfile now reuses pylock.toml across compatible Python targets, including after kernel updates. Reuse requires unchanged requirements, index, and requires-python, compatible wheels, and a locked dependency graph matching the target's markers #14843.
The lockfile's environments marker now includes only the interpreter version and marker variables used by the dependency graph. Without --frozen-lockfile, pnpm warns and resolves again when the locked graph no longer matches the target.
Python resolution no longer fails on malformed Requires-Python values, such as the trailing comma in openpyxl 3.0.x. pnpm treats these releases as declaring no interpreter range #14910.
pnpm add pypi:... now rejects unsupported --save-prefix values before editing the manifest or resolving dependencies.
Scripts listed in syncInjectedDepsAfterScripts no longer fail with ERR_PNPM_INJECTED_DEPS_SYNC_READ_DIR when the lockfile contains an injected package copy that no project depends on.
shellEmulator now expands ${VAR}, ${VAR:-default}, and ${VAR:+alternative} in scripts #14814.
Cargo and Python project discovery now honors ! exclusions in pnpm-workspace.yaml packages, skipping both parsing and generated source configuration for excluded projects #14844.
pnpm --filter "./packages/{app,lib}" now selects either alternative. Brace alternatives can nest, span path separators, and combine with other wildcards.
GitHub Actions updates now stop if an action reference changes during version resolution, and preserve unrelated workflow edits.
pn, pnpx, and pnx now run the pnpm installed alongside them, even when that directory is absent from PATH or another pnpm comes first #14803.
pnpm --version now reports failures to install or record a project's pinned pnpm, then prints the running CLI's version. It also honors --store-dir and --store #14831.
pnpm self-update no longer reinstalls the active version when it was installed by the standalone installation script #14823.
pnpm t and pnpm tst work again as aliases for pnpm test.
pnpm sbom now emits valid repository URLs in CycloneDX externalReferences[].url and SPDX homepage. Shorthands such as vercel/ms become git+https URLs, embedded credentials are removed, and invalid repository values are omitted #14773.
pnpm 12.4.1 fixes installs that failed on filesystems refusing hard links or clones, on Android, and under nodeLinker: hoisted . Repeat installs are f
pnpm 12.4.1 fixes installs that failed on filesystems refusing hard links or clones, on Android, and under nodeLinker: hoisted. Repeat installs are faster.
pnpm install no longer fails with Operation not permitted when the filesystem refuses a hard link or a copy-on-write clone #14722. Under packageImportMethod: auto and clone-or-copy, pnpm copies the file instead. EdenFS checkouts, which have no hard links, and rootless containers, which refuse the clone syscall, both hit this. An explicit packageImportMethod: hardlink or clone still reports the error.
pnpm also copies a package file whose store entry has reached the filesystem's limit on names for one file, 1024 on NTFS and 65000 on ext4. Such a file failed the install under packageImportMethod: hardlink, and under auto it stopped pnpm hard linking for the rest of the install.
pnpm install no longer writes a package file through a symlink left at the path it is importing to. Copying such a file overwrote whatever the link pointed at, and created that file when the link pointed nowhere. An executable package file also made the link's target executable.
Fixed pnpm install and pnpm dlx on Android. Registry requests crashed because pnpm found no system CA certificates, so pnpm uses bundled ones there #14777. Imports also failed with "Permission denied" on filesystems that deny hard links and reflinks, and now fall back to copying #14780.
pnpm install no longer fails with "Invalid cross-device link" while preserving a package's nested node_modules directory during a Docker build #14758.
pnpm install no longer fails on a package tarball that carries a file at the archive root, such as the ._* entries macOS tar adds #14701. The file is installed at the root of the package.
A file: tarball packed without the usual package/ directory is now recorded under the name and version from its own package.json. It was recorded under the alias the dependency was given, at version 0.0.0.
Under nodeLinker: hoisted, pnpm install no longer re-imports packages that are already in place. A repeat install replaced the whole node_modules tree and reported Packages: +N. A package is still imported when its directory is missing, when its package.json no longer carries the installed version, when it is a file: dependency, and when it is patched. Lifecycle scripts no longer run again for a package left in place, and pnpm rebuild and a change to allowBuilds still reach it.
pnpm install now runs a dependency's build scripts again when its side-effects cache entry has no files to restore #14717. Such builds were skipped and nothing was put in their place, so a script whose whole effect lands outside its own package directory, such as a git hook installer, never took effect. pnpm no longer publishes empty artifacts to the shared side-effects cache either.
pnpm install, pnpm add, and pnpm dedupe now apply ignoredOptionalDependencies #14729. Matching optional dependencies are left out of the lockfile and are not installed. pnpm 12 installed them whenever it resolved dependencies from scratch.
pnpm install no longer links a transitive dependency to a workspace package when linkWorkspacePackages is true and the dependency is declared with a plain version range #14781. Enabling preferWorkspacePackages does not change this. Set linkWorkspacePackages: deep to link them.
pnpm install no longer leaves dangling dependency links in workspace packages located above the workspace root #14726.
pnpm install and pnpm add no longer leave a dangling symlink in node_modules when a project starts depending directly on a package that the lockfile holds only as a transitive dependency with resolved peer dependencies #14714.
pnpm dedupe now keeps a compatible auto-installed peer when another workspace project depends on a newer major #14697. Repeated runs alternated between compatible and incompatible peer versions.
pnpm peers check no longer reports a peer dependency declared as workspace:^, workspace:~, or a bare workspace: as unmet #14770. pnpm reported these as unmet whatever version the linked workspace project supplied.
Sped up repeat installs #14540. pnpm checks the store's files only for the packages it links into node_modules, instead of every package in the lockfile. Creating the command shims in node_modules/.bin makes about 1,500 fewer filesystem calls in a 76 project workspace. Installs that use the global virtual store read their slot paths from the cache directory instead of deriving them every time. Verifying a large lockfile also allocates less memory.
Sped up pnpm install in Cargo workspaces with many member crates. Repeated installs reuse verified Cargo checksum metadata.
Installing several packages from the same Git repository and commit now downloads the source once per install #14725. Each package still runs its prepare scripts in its own copy of the checkout.
pnpm now passes Ctrl+C on to the script or command it started and waits for it to shut down #14723. pnpm exited first, so a script that was still writing landed on the shell prompt.
pnpm run "/pattern/" --no-bail now lets every matched script finish after one of them fails #14718. The command exits with ERR_PNPM_RUN_FAILED, and its message lists the scripts that failed in the order they were selected.
pnpm pipeline no longer fails on a project that tracks a symlink, such as a CLAUDE.md pointing at AGENTS.md #14692. Changing a symlinked input's target invalidates that task's cache, and pnpm pipeline --no-cache no longer hashes task inputs.
pnpm add -g, pnpm update -g, and pnpm remove -g no longer change global bins or install directories after reading only part of an installed package group #13796. If any declared package manifest is missing, malformed, or unreadable, pnpm now fails before it activates or removes anything and leaves the existing global installation intact.
pnpm dedupe now processes every workspace project by default, including workspaces that keep a separate lockfile per project #14732. Workspace filters select which projects it processes, and --fail-if-no-match exits with an error when no project matches.
pnpm update <name>@<version> now keeps the range operator the manifest declares #14745. Running pnpm update react@19.3.0 on "react": "^19.2.8" writes "react": "^19.3.0". A jsr: entry keeps its jsr: prefix, and a plain pnpm update now moves a jsr: range the way it moves an npm range.
pnpm --filter directory selectors now support ? wildcards and character classes such as [ab]. A * or ? wildcard no longer selects a directory whose name starts with a dot, as on pnpm 11.
pnpm deploy --legacy now prefers the dependency versions pinned in the source workspace lockfile when they still satisfy the deployed project's range #13857.
pnpm sbom now leaves out a package's author field when the manifest author name is empty or contains only whitespace #14685. In a filtered or split workspace run, only a project with no author field inherits the workspace root's author.
pnpm sbom --sbom-format spdx now writes creationInfo.created with whole seconds, such as 2026-09-08T10:38:21Z #14684. The fractional seconds it carried were rejected by strict SPDX consumers.
The updateConfig pnpmfile hook now receives the resolved configuration, including settings that came from .npmrc, the command line, or a default #14676. Scoped registries are reported under registriesByScope, and a hook may rewrite that map to change where packages are fetched from. Registry credentials are reported under configByUri, as pnpm 11 reports them. An unset setting is left out rather than reported as null.
pnpm audit --fix and the minimumReleaseAgeStrict approval prompt now keep the comments in minimumReleaseAgeExclude when they append an entry to it in pnpm-workspace.yaml. The rest of the list is left as written, and the trustPolicyExcludePrune and minimumReleaseAgeExcludePrune cleanups keep the comments of the entries they retain.
pnpm install and pnpm dedupe now run those cleanups too #14759. Only pnpm add, pnpm update, and pnpm remove pruned the entries that the freshly written lockfile no longer resolves.
pnpm config set --global node-download-mirrors no longer rejects the key #13611. The global config file already accepted nodeDownloadMirrors, but the command refused to write it.
NO_PROXY entries that start with a dot, such as .npmjs.org, now bypass the proxy for the domain and its subdomains #14686.
pnpm no longer creates a project pnpm-lock.yaml when devEngines.packageManager.onFail is download and lockfile writing is off through lockfile: false or --no-lockfile #14728. pnpm still switches to the pinned version.
pnpm now writes node_modules/.package-map.json only when nodeExperimentalPackageMap is enabled. Nothing reads the file without that setting, and an install that stops writing the map removes the one a previous install left.
pnpm pipeline no longer fails with intermittent access denied errors when concurrent tasks save their cache entries on Windows.
Windows filesystem operations now retry permission errors for up to one second #14682. A permanent permission error delayed the failure by a minute. Sharing and lock violations keep their one minute retry budget.
pnpm now warns when the root package.json declares a non-empty workspaces array and the project has no pnpm-workspace.yaml #2255. Such an install linked no project and said nothing about why.
ERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIR now names the file or directory in node_modules that pnpm could not clean up. It reported only the underlying OS error, such as "Access is denied (os error 5)".
pnpm --help no longer describes pnpm as experimental.
This tag was signed with the committer’s verified signature .
zkochan Zoltan Kochan
GPG key ID: 649E4D4AF74E7DEC
Verified Learn about vigilant mode .
pnpm can now manage npm, Python, and Cargo dependencies in the same workspace. Enable python.enabled or cargo.enabled in pnpm-workspace.yaml, then use
pnpm can now manage npm, Python, and Cargo dependencies in the same workspace. Enable python.enabled or cargo.enabled in pnpm-workspace.yaml, then use pnpm install to install them together.
pnpm add pypi:<package>. pnpm uses pyproject.toml, pylock.toml, and a managed .venv. Frozen and offline installs are supported, and pnpm run and pnpm exec make the environment's executables available #14566.pnpm add crate:<package>. pnpm supports crates.io and custom sparse registries configured with cargo.indexUrl. Registry authentication supports pnpm credentials and, for crates.io, CARGO_REGISTRY_TOKEN or $CARGO_HOME/credentials.toml.Both ecosystems support faster dependency resolution through pnprServer, with local resolution as a fallback when the server does not support it.
Added pnpm pipeline [name] to install frozen dependencies and run workspace tasks declared in pipelines. It selects affected projects, runs their task graph, and continues running tasks after a task fails.
Tasks support inputs, outputs, env, and cache settings. Cached results restore task outputs and replay logs. Cargo tasks can reuse local build state between worktrees with tasks.<name>.cargoTargetDir. Set includeWorkspaceRoot: true to include root tasks.
Use pnpm pipeline --dry-run to preview the task graph without installing configuration dependencies or running workspace hooks.
Added support for Android on arm64 and x64, FreeBSD on x64, and Linux on ppc64le, s390x, and RISC-V (riscv64 with glibc) #14431, #14597, #7582.
Added trustPolicyExcludePrune to automatically remove unused versions and packages from trustPolicyExclude when running pnpm add, pnpm update, or pnpm remove. It is disabled by default. Package name patterns such as @scope/* are kept, and cleanup is skipped when sharedWorkspaceLockfile is false.
Added pnpm change check for CI validation of package versions against the versioning.epics bands and versioning.fixed groups in pnpm-workspace.yaml. It reports all violations, including packages that are not part of the current release.
Registry metadata is now kept separate for registries with different URL paths or schemes. This prevents installs from using another registry's package versions or tarball URLs, and keeps metadata fetched over HTTP from being reused for HTTPS #13558.
The first install after upgrading refetches registry metadata. The package store is unchanged. pnpm cache view now shows full registry URLs. Scripts that parse the directory names from pnpm cache list-registries or pnpm cache list need updating.
Patches that add build scripts or a binding.gyp now trigger a build, subject to build approval. Unapproved builds appear under "Ignored build scripts" #14648.
Build scripts can now be rejected before installing a package with pnpm add --allow-build=!<pkg>, including global installs. pnpm approve-builds <pkg> and pnpm approve-builds !<pkg> also save decisions when no packages are awaiting approval. They warn if the named package is not awaiting approval #14067.
A registry configured in .npmrc now takes precedence over registry settings saved by pnpm login in the global config.yaml. This fixes installs using the wrong registry after login #14614.
Large downloads over slow connections no longer time out while data is still arriving. fetch-timeout now limits how long a request can go without making progress #14604.
Sped up installs in workspaces with many projects when reusing a warm global virtual store #14540.
pnpm deploy is faster in large workspaces and no longer fails with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH when the project includes a .pnpmfile.mjs #14539, #14671.
pnpm add --workspace <pkg> works again. It saves the dependency with the workspace: protocol and links it from the workspace. The command fails if no workspace project provides the package #14602.
pnpm add and pnpm install now accept protocol-prefixed selectors such as jsr:@scope/pkg, npm:pkg@^1.0.0, and workspace:pkg@* #14590. Installs with JSR dependencies in the lockfile also no longer fail with ERR_PNPM_META_FETCH_FAIL #14649.
Boolean flags now accept explicit inline values. For example, pnpm install --prod=false installs devDependencies, while --prod=true skips them #14553.
pnpm install <pkg> now accepts --offline and --prefer-offline, as pnpm add <pkg> already did #14194.
Fixed pnpm install --frozen-lockfile rejecting a freshly generated lockfile when overrides use relative file: or link: paths in a workspace #14555.
Fixed installs with config dependencies failing on symlinked lockfiles, such as those used by Bazel and Nix, when the config dependencies have not changed. Updates that would write through a symlink remain disallowed. Updating config dependencies also preserves lockfiles that start with a byte order mark #14372.
Fixed package manager version pins being written to the wrong lockfile when lockfileDir is set. The pins also remain consistent across commands when version switching is disabled, avoiding unnecessary lockfile changes #14633, #14575.
pnpm import now respects lockfileDir and branch lockfiles without modifying other lockfiles. Failed imports restore the destination lockfile #14563.
pnpm patch-commit now produces valid patches when files are added or deleted. pnpm install also accepts patches that delete files without listing their contents, and patch files with CRLF line endings #14559, #14557.
Fixed version ranges with partial upper bounds. For example, <=16 now includes all 16.x versions, and >=0.11 <=3 correctly accepts 3.0.1 #14419.
Workspace package patterns now support . and .. segments and repeated slashes. Patterns such as ./packages/* and exclusions such as !./packages/foo now match correctly #14571.
packageConfigs settings now apply to the specified projects when sharedWorkspaceLockfile is false, including overrides, hoist, modulesDir, saveExact, and savePrefix. Workspaces with a shared lockfile report which entries were ignored #14556.
pnpm run and pnpm exec no longer report a changed workspace structure after a successful install when sharedWorkspaceLockfile is false and verifyDepsBeforeRun is enabled #14588.
Commands run from a project's subdirectory now find the nearest ancestor with a manifest. This fixes commands such as pnpm bin returning paths under the wrong directory. pnpm init still creates its manifest in the current directory, and pnpm exec still runs there #14622.
Relative scriptShell paths in pnpm-workspace.yaml now resolve from the workspace root, including when scripts run in nested packages. Bare command names such as bash still use PATH #14422.
Fixed installing the pnpm version pinned in packageManager when nodeLinker is hoisted. Managed Node.js, Deno, and Bun installations also work when the global config uses nodeLinker: hoisted #14595.
The JavaScript pnpm can again switch to a project's pinned pnpm version on platforms without a native binary for that version, such as Alpine Linux with pnpm 10 or Intel Macs with pnpm 11. If a native pnpm version does not support the platform, the error now names the missing target #13622.
Provisioning Yarn 6 now uses GH_TOKEN or GITHUB_TOKEN when available to avoid GitHub's anonymous API rate limit in CI. Tokens are only sent when strict-ssl is enabled.
Fixed concurrent installs sharing a global virtual store on macOS failing with "failed to import ... No such file or directory" #14560.
Fixed pnpm setup failing with ERR_PNPM_DIRECTORY_FETCHER_PATH_ESCAPE on Windows. Local file: dependencies whose directories are symlinks or junctions are now packed correctly #14618.
On Windows, installs now retry replacing command shims temporarily locked by another process #14549.
Fixed argument forwarding on Windows with shellEmulator enabled. Paths ending in a backslash, line breaks, and literal shell expressions are preserved #14548.
Windows store paths now consistently use backslashes in pnpm store path output and in the storeDir and virtualStoreDir fields of node_modules/.modules.yaml.
Invalid certificates in ca or cafile no longer cause an Invalid CA certificate error. Valid certificates still apply, and blank cert or key values are treated as unset #14646.
Installs now respect the archive extraction concurrency limit even after a download is abandoned #14585.
pnpm audit summaries now exclude advisories ignored through auditConfig.ignoreGhsas and report them separately. When all advisories are ignored, the summary says so #14535.
pnpm pack --json now reports errors as JSON. Lifecycle script output appears before the final JSON output.
pnpm outdated -r now wraps the Dependents column, keeping the table readable when many workspace projects use the same dependency #14591.
Shell completions now support the pn alias in bash, fish, pwsh, and zsh #11955.
pnpm version now accepts -m as a short alias for --message #14567.
Sped up dependency resolution in large workspaces #14352 .
Sped up dependency resolution in large workspaces #14352.
pnpm 12 now accepts the boolean settings as command-line flags on every command that takes them in pnpm 11, for example pnpm install --unsafe-perm, pnpm add foo --offline, and pnpm install --dangerously-allow-all-builds. pnpm 12 rejected them with unexpected argument, which failed every install on Vercel, whose build runs pnpm install --unsafe-perm #14346.
pnpm remove now accepts --unsafe-perm, the same flag pnpm install, pnpm add, and pnpm update take.
This tag was signed with the committer’s verified signature .
zkochan Zoltan Kochan
GPG key ID: 649E4D4AF74E7DEC
Verified Learn about vigilant mode .
Fixed concurrent installs sharing a store occasionally failing with an ENOENT error while importing a package file #14353 .
Fixed concurrent installs sharing a store occasionally failing with an ENOENT error while importing a package file #14353.
Sped up writing the lockfile in large workspaces #14352.
Sped up dependency resolution in large workspaces #14352.
pnpm now runs through Node.js when it was installed by a tool that skips build scripts, such as Vercel's packageManager provisioning, Bun, Deno, or npm install --ignore-scripts. Those installs previously failed with syntax error near unexpected token ')'. They still cannot run pnpm on Windows. On macOS only a shell can start it #14346.
pnpm audit --fix update no longer aborts when a vulnerable package has no safe version inside its declared range #14508 . The run updates every packag…
pnpm audit --fix update no longer aborts when a vulnerable package has no safe version inside its declared range #14508. The run updates every package it can and lists the rest as remaining.
pnpm install no longer reruns root lifecycle scripts when the global virtual store contains an unfinished-build marker in a package slot that the current lockfile does not use pnpm/pnpm#14485.
Sped up installs that have no lockfile. pnpm now links packages whose dependency subtree has no peer dependencies into the virtual store while resolution is still running.
pnpm run and pnpm exec now start without reinstalling on filesystems that keep sub-millisecond mtimes, such as NTFS. Previously, every run on those filesystems reinstalled first pnpm/pnpm#14486.
pnpm import now keeps the versions recorded in package-lock.json, npm-shrinkwrap.json, or yarn.lock when it generates pnpm-lock.yaml. A range in package.json, a catalog, or an override still decides which versions are eligible, and the recorded version is preferred among them. The generated lockfile previously could pin newer versions than the source lockfile #14476.
pnpm import in a workspace now imports every workspace project into the shared lockfile. It previously imported only the project in the current directory.
pnpm import now fails with ERR_PNPM_LOCKFILE_NOT_FOUND when none of the three source lockfiles is present. It also fails with ERR_PNPM_YARN_LOCKFILE_PARSE_FAILED when it cannot parse yarn.lock. It previously generated a lockfile from scratch in both cases.
pnpm import always resolves locally. It warns when --pnpr-server or the pnpr-server setting is given and does not use the server.
Sped up installs in large workspaces. Discovering the workspace projects no longer enumerates every matched directory to learn which manifest files it holds #14352.
Sped up installs in large workspaces. The resolver and the peer pass allocate less for every dependency edge #14352.
pnpm self-update, pnpm with, and automatic package-manager version switching no longer wait through registry retry delays when a configured registry has no signatures and registry.npmjs.org is unavailable #14483.
Sped up installs in large workspaces. Saving the lockfile is faster, and the install finishes without waiting for memory cleanup #14352.
pnpm install now relinks workspace packages when publishConfig.linkDirectory changes. Frozen installs report an outdated lockfile until it is regenerated pnpm/pnpm#14488.
The pnpm npm wrapper keeps its placeholder shebang-less so pnpm 11 can install pnpm 12 through the version store. Wrapper installs must allow lifecycle scripts to install the native binary #14502.
Sped up dependency resolution when there is no lockfile, and for the dependencies a lockfile does not cover.
Sped up installs in large workspaces. Workspace link: targets and importer ids are now derived from the paths' suffixes under the workspace root #14352.
pnpm install now reports "Already up to date" when local tarball dependencies have not changed #14495.
pnpm update now accepts --ignore-scripts and skips lifecycle scripts during the update pnpm/pnpm#14512.
Sped up installs that restore a deleted node_modules from a warm global virtual store. pnpm no longer re-links packages that are already fully present in the global virtual store #14510.
Sped up installs in large workspaces: the anchor for re-rendering workspace link: targets is now derived once per project instead of once per dependen
Sped up installs in large workspaces: the anchor for re-rendering workspace link: targets is now derived once per project instead of once per dependency edge, and project ordering hashes paths by their raw bytes #14352.
After a self-update from pnpm 12.2 to 12.3, global commands such as node, npm, and yarn failed with unexpected argument '--shim' found. Global commands now launch normally, and their first launch migrates the global bin directory to native shims. When self-update downgrades to pnpm 12.2 or older, it keeps the newer native shims so those commands continue to work.
Sped up installs in large workspaces. The check that verifies each project against the lockfile now runs the projects in parallel #14352.
Every context-aware global command (node, deno, bun, and the shims created with pnpm shim add) is now a native executable on every platform, so enviro
Every context-aware global command (node, deno, bun, and the shims created with pnpm shim add) is now a native executable on every platform, so environment variables whose names are not valid shell identifiers reach these commands. On Windows, <name>.exe replaces the .cmd and .ps1 shims for them. Shims written by earlier pnpm 12 releases are migrated on the next global install or self-update.
pnpm remove and pnpm update now accept --trust-lockfile, --no-trust-lockfile, --trust-policy, --trust-policy-exclude and --trust-policy-ignore-after, the same flags pnpm install and pnpm add take, so the supply-chain settings can be overridden for a single run. pnpm remove verifies the lockfile against the active policies the way pnpm install does, and --trust-lockfile skips that pass for every entry, not only the package being removed.
pnpm now also honors --config.trust-lockfile=<value>, and accepts the bare --trust-lockfile / --no-trust-lockfile spelling on the commands that previously took the setting from the config file alone.
pnpm add <local directory>, pnpm add <local tarball>, pnpm add file:<path> and pnpm add <tarball URL> work again. A specifier given without a <name>@ prefix is no longer read as a registry package name and rejected with ERR_PNPM_PACKAGE_MANAGER_ADD_RESOLVE_LATEST #14437.
Fixed pnpm deploy --legacy ignoring allowUnusedPatches supplied through --config.allow-unused-patches or the PNPM_CONFIG_ALLOW_UNUSED_PATCHES environment variable pnpm/pnpm#14450.
Fixed pnpm install --lockfile-only writing a lockfile that referenced a missing peer-suffixed snapshot when an npm-aliased dependency took part in a cyclic peer dependency graph. The following pnpm install --frozen-lockfile failed with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY #14449.
pnpm config now accepts -g/--global, --location, and --json before its subcommand pnpm/pnpm#14421.
pnpm dedupe now converges in one pass when it re-resolves a lockfile created by pnpm 11, so a second run no longer changes the lockfile #14455.
Fixed detached child processes being terminated on Windows when another program launches pnpm directly, without a shell, as nr from @antfu/ni does #14447.
Fixed pnpm docs <package>@<version> ignoring the requested version. It now opens the selected version's homepage and reports a missing version instead of opening the package-level homepage pnpm/pnpm#14428.
Sped up installs in large workspaces. pnpm-lock.yaml is now read while the workspace projects are being discovered #14352.
Fixed filtered and recursive pnpm run and pnpm exec hanging when a script reads from the terminal. Interactive prompts work again in a script that pnpm never runs alongside another one, such as a single --filtered project, --workspace-concurrency=1, a dependency chain, or a task declaring concurrency: 1 #14397.
Fixed false unmet peer errors for auto-installed peers in linked workspace packages.
Fixed npm global installs on Windows so the PowerShell shims invoke pnpm.exe.
Fixed pnpm with current <command> when global options precede it, such as pnpm --workspace-root with current --version pnpm/pnpm#14413.
A short-option cluster that mixes a global flag with an option owned by the command, such as pnpm -ro dist pack-app, is now parsed like the same options written after the command.
An option written before the command name is now reported as an unknown option unless that command accepts it, instead of being taken for the command to run. pnpm -P exec echo and pnpm -z exec echo fail the way pnpm --tag next exec echo does.
Apply pure insertions in zero-context patches at the correct line instead of one line early.
Improved peer dependency resolution performance when many packages reuse the same peer ranges.
pnpm outdated and pnpm update now follow local actions and reusable workflows referenced with GitHub's self-repository syntax (uses: $/.github/actions/setup) when looking for outdated GitHub Actions, the same way they follow ./ references.
The pnpm install --help descriptions of --prod and --dev no longer claim that the flags take precedence over NODE_ENV. pnpm does not read NODE_ENV when selecting which dependency groups to install #14445.
Sped up installs in large workspaces. The check that decides whether the lockfile needs updating no longer compares every project against every lockfile entry #14352.
Sped up dependency resolution in large workspaces that use link: dependencies #14352.
On Linux, pnpm now resolves registry hostnames through the system resolver (getaddrinfo), as it already does on macOS and Windows and as pnpm 11 did. Previously, an /etc/resolv.conf containing an option the bundled pure-Rust resolver did not recognize, such as options no_tld_query, made pnpm ignore the configured nameservers and silently query Google's public DNS instead #14469.
Sped up dependency resolution in large workspaces. The resolver builds fewer lookup keys for each dependency #14352.
catalogMode and --save-catalog no longer move a local path, tarball, or workspace:<path> specifier into a catalog. Such a specifier is resolved against the project that declares it, so one catalog entry cannot mean the same directory for every project that references it #14437.
Sped up installs in large workspaces. The workspace dependency graph is now built once per run instead of twice #14352.
Sped up writing pnpm-lock.yaml in large workspaces #14352.
Fixed non-frozen installs through a pnpr server failing instead of regenerating a conflicted lockfile.
pnpm update --interactive renders its checklist the way pnpm 11 does. Group headings and column headers are separators the cursor skips instead of checkboxes that select nothing. The columns of one group line up with the next. a toggles all and i inverts the selection. The confirmed selection is echoed as a list of package names #14423.
Fixed pnpm config commands targeting global configuration to skip project package manager version switching, allowing registry authentication to be configured before pnpm downloads a project-pinned version pnpm/pnpm#14463.
Fixed pnpm retaining the surrounding quotes in .npmrc values, including auth tokens expanded from environment variables. This restores authentication with registries configured using :_authToken="${TOKEN}" pnpm/pnpm#14427.
Fetch and tarball errors no longer print the secrets of the URL they name. Inline user:pass@ credentials and the query string or fragment of a signed URL are hidden, so a failed install or pnpm add <url> cannot leak them into terminal scrollback or CI logs.
When dist-tags.latest names a version whose manifest pnpm cannot read, the error now names that version and the field it could not decode, instead of reporting the tag as empty.
Retry transient Windows file-lock errors, including sharing violations, while linking dependencies with the default (isolated) nodeLinker. This fixes pnpm/pnpm#14407.
pnpm run, pnpm exec, pnpm rebuild, and the script shortcuts such as pnpm test now load the pnpmfile, so updateConfig hook settings such as extraEnv and extraBinPaths reach the scripts they spawn #14433.
The pnpm executable of the npm package now works when the package was installed without running its install scripts, as under --ignore-scripts or the default build-script block of pnpm and Bun #14346. In that case it runs through Node.js and, in a terminal, says how to switch to the native binary.
Sped up installs in large workspaces. The resolver no longer copies the whole lockfile before resolving #14352.
minimumReleaseAgeStrict now defaults to true when minimumReleaseAge is explicitly configured, whether in pnpm-workspace.yaml, the global config.yaml, a PNPM_CONFIG_* variable, or a CLI flag. The built-in 1440-minute default stays non-strict. Previously an explicit cutoff was treated as non-strict, so immature versions were silently added to minimumReleaseAgeExclude instead of being gated with a prompt #14409.
Preserve environment variables whose names are not valid shell identifiers when launching Node.js installed by pnpm runtime set node --global on Unix pnpm/pnpm#14417.
Fixed pnpm repo and pnpm docs failing to open the Windows browser from WSL pnpm/pnpm#14467.
pnpm link, pnpm outdated, and pnpm import now apply pnpmfile updateConfig hooks before resolving dependencies.
Fixed standalone installations to preserve the bundled node-gyp files used to build native dependencies.
Fixed resolution against registries whose version manifests carry _npmUser, dist.attestations, dist.unpackedSize, dist.fileCount, or peerDependenciesMeta in a shape npm does not use. Such a version was skipped as though it had never been published, so pnpm add could fail with "no version found for the latest tag" even though the registry served it.
pnpm unpublish now completes the two-factor authentication a registry asks for instead of failing with ERR_PNPM_UNAUTHORIZED while logged in. A 401 that is an OTP challenge starts the web-based authentication flow, or prompts for a classic one-time password. The obtained password is reused by every request of the run #14464.
On Windows, pnpm now resolves host names through the system resolver instead of its own DNS client. The built-in client bound a UDP socket for every lookup, which made Windows Defender Firewall ask to allow pnpm.exe again after every pnpm self-update #14405.
Restored the pnpm executable target without a file extension so pnpm 12.1 and earlier can upgrade to newer pnpm 12 releases on POSIX systems.
Fixed pnpm audit --fix=override ignoring the saveExact and savePrefix settings when writing vulnerability overrides #11523.
workspace: protocol.Fixed pnpm audit --fix failing with ERR_PNPM_INVALID_FIX_OPTION when used without a value, including when another flag follows it, as in pnpm audit --fix --json #13261. Fixed pnpm audit --fix=override ignoring the saveExact and savePrefix settings when writing vulnerability overrides #11523.
Authenticate Node.js runtime downloads from nodeDownloadMirrors with URL-scoped npm registry credentials, including bearer tokens, basic auth, and tokenHelper pnpm/pnpm#14334.
Fixed detached child processes being terminated after successful commands on Windows.
Sped up installs in large workspaces by resolving each named workspace: dependency (workspace:*, workspace:^, workspace:1.2.3) once and reusing it across every project that declares it, instead of re-resolving it per project.
Fixed pnpm install --fix-lockfile to derive its repair and filtered-merge views from one lockfile snapshot.
Load pnpmfile updateConfig hooks before packing so hook-provided catalogs resolve in pnpm pack, pnpm publish, and pnpm stage publish pnpm/pnpm#14377.
pnpm deploy no longer requires injectWorkspacePackages to be enabled. A linked workspace dependency is rewritten to a file: dependency in the dedicated deploy lockfile, and the peer dependencies it declares are bound to the deployed graph's own resolution.
When a peer resolves to more than one version in that graph the binding is ambiguous, and choosing between the candidates is exactly what injecting the package would have decided, so the deploy still fails — now with ERR_PNPM_DEPLOY_AMBIGUOUS_PEER, which names the package, the peer, and the competing versions, instead of refusing every non-injected workspace up front, and suggests pinning the peer to one version with an overrides entry as the way to keep deploying without injection #9386.
Fixed global virtual store hashes for dependency cycles. Every package that transitively depends on an allowed build now includes the engine in its store path, independent of traversal order pnpm/pnpm#14341.
Fixed ERR_PNPM_CMD_SHIM_CHMOD when several installs run at once against a shared global virtual store. One install could remove a command shim while another was making it executable (pnpm/pnpm#14353).
Fixed the PowerShell shim generated by npm install -g pnpm on Windows so it invokes the native pnpm.exe binary pnpm/pnpm#14362.
Fixed context-aware global shims on WSL2 so native Linux installations dispatch through the project runtime.
pnpm install no longer writes global minimumReleaseAgeExclude entries to the project's pnpm-workspace.yaml pnpm/pnpm#14347.
Fixed catalog: ranges in workspace package peer dependencies being reported as unmet pnpm/pnpm#14361.
globalDir and globalBinDir are honored wherever they are set, so pnpm add -g no longer fails with ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH after pnpm config set -g global-bin-dir #14336. The global config.yaml is read again, PNPM_CONFIG_GLOBAL_DIR / PNPM_CONFIG_GLOBAL_BIN_DIR reach the directories derived from them, and a leading ~/ is expanded before that derivation. A project's pnpm-workspace.yaml still cannot set either key.
Fixed the install progress line reporting added 0 under nodeLinker: hoisted, even when packages were linked into node_modules #14348.
An auto-installed optional peer is now resolved to a version its declared peer range accepts, even when the workspace root depends on that package at a version outside the range. Previously the root's version was used and then reported as an unmet optional peer #13867.
Fixed pnpm run "/pattern/" running matching scripts one at a time in a single project. Matching scripts now run concurrently up to workspaceConcurrency, and their output is prefixed so concurrent lines remain distinguishable pnpm discussion 14357.
Fixed a slowdown at the end of a resolving install in a large workspace. The peer-dependency report now inspects only the projects the resolution flagged, rather than every project in the lockfile (pnpm/pnpm#14359).
Speed up workspace discovery for literal directories and conventional trailing-star patterns.
Workspace patterns now follow the same dot-directory rule as pnpm 11: a wildcard no longer matches a dot-prefixed directory, so packages/* and ** skip packages/.cache and .git. A pattern that names a dot-prefixed directory still matches it, as packages/.cache and packages/.* do.
pnpm audit now ends its output with a trailing newline, including the --json, --fix, and --ignore output.
Retry transient Windows file-lock errors while replacing hoisted packages during installation. This fixes pnpm/pnpm#14349.
Fixed command-line --side-effects-cache overrides being ignored when pnpm-workspace.yaml uses the object form of sideEffectsCache pnpm/pnpm#14338.
Speed up workspace project discovery in large monorepos: workspace patterns are now probed concurrently and the discovered projects' package.json files are read in parallel #14352.
Fixed repeated pnpm dedupe runs alternating between peer resolutions when a peer is provided through an npm alias.
Fixed pnpm repo <package> and pnpm docs <package> resolving bare package names through the latest tag, and prevented malformed package ranges from crashing registry selection.
Fixed non-ASCII characters in configuration values being mangled during environment-variable substitution. Paths such as storeDir: ./café-store are now preserved #14383.
pnpm login and pnpm adduser now record the granted token in the global config.yaml, under the _auth setting, with --scope's scope routed to that regis
pnpm login and pnpm adduser now record the granted token in the global config.yaml, under the _auth setting, with --scope's scope routed to that registry under registries. pnpm logout removes it from there, and still from an auth.ini an earlier version wrote. Tokens already in auth.ini keep working.
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.
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.
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
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).Added macOS and Windows x64 and arm64 support to remote shared build artifacts pnpm/pnpm#13771.
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.
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.
Fixed pnpm deploy --legacy to exclude dependencies that are only reachable from unselected workspace projects after pnpm fetch.
Fixed dependency-verification install logs corrupting pnpm exec output and ignoring --silent pnpm/pnpm#14197.
pnpm clean / pnpm purge run from a workspace subdirectory now remove each project's own node_modules instead of emptying the workspace root's for every project #14239. A custom modulesDir is resolved against each project directory too.
pnpm dlx <pkg>@catalog: now resolves the specifier through the calling workspace's catalogs instead of failing with ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC #14294.
Fixed pnpm doctor reporting a version that does not match pnpm --version pnpm/pnpm#14225.
Pacquet now strips exactly one leading path component from ./-prefixed tarball entries, matching pnpm and npm's tar extraction semantics and keeping shared store keys consistent.
Installs whose lockfile carries platform or engine constraints are up to ~150 ms faster when resolution runs: the node --version probe behind the installability checks now starts before the lockfile is parsed and finishes while dependencies resolve, instead of running afterwards.
Treat empty scripts selected by a regular expression as missing before running dependent tasks.
Filter hidden scripts matched by a regular expression during recursive runs when a visible script also matches.
Fixed .mjs pnpmfile hooks failing to load on Windows, including hooks supplied by config dependencies pnpm/pnpm#14301.
Fixed automatically switched pnpm versions forcing all descendant pnpm processes to use the same version pnpm/pnpm#14309.
Fixed pnpm deploy --prod failing when an excluded dev dependency was also declared as an optional peer dependency pnpm/pnpm#14302.
Fixed pnpm pack to respect the files field when deciding whether to include root-level changelog, history, and notice files.
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.
When multiple versions of the same package expose the same binary, pnpm now links the binary from the highest version #14249.
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.
Allowed pnpm's shared-artifact client to connect to an artifact-only pnpr tier.
Rebuilding node_modules from an up-to-date lockfile is up to ~200 ms faster: the node --version probe that installability checks and store keying need now runs concurrently with the store's warm-cache reads instead of before them.
Remove the duplicate colon from the one-time password prompt.
Print errors as JSON on stdout when --json is passed to pnpm view or its aliases (info, show, and v).
Installs complete faster on workspaces with many projects: each project's node_modules is now linked concurrently.
Fixed patchedDependencies matching for git-hosted dependencies during fresh and frozen installs pnpm/pnpm#14273.
pnpm pm <command> works again: the pm prefix, which forces pnpm's built-in command over a package.json script of the same name, is recognized instead of failing with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL / Command "pm" not found. pnpm pm clean and pnpm pm purge now remove node_modules even when the project (or the workspace root) declares a clean / purge script #14226.
The settings that pnpm accepts as command-line flags are recognized again: --package-import-method, --hoist-pattern, --public-hoist-pattern, --no-hoist, --global-dir, --virtual-store-dir, --modules-dir, --child-concurrency, --no-lockfile, --strict-peer-dependencies, --side-effects-cache, --side-effects-cache-readonly, --trust-policy, --trust-policy-exclude, --trust-policy-ignore-after, and --optimistic-repeat-install. Each is accepted anywhere on the command line, spelled either --setting=value or --setting value, and overrides the same setting read from pnpm-workspace.yaml or .npmrc #14281.
pnpm add, pnpm update, and pnpm remove now save package.json before failing with ERR_PNPM_IGNORED_BUILDS. The dependency they were asked to change is already materialized by that point, so the manifest has to record it — otherwise the next install removes the packages again.
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.
Recognize pnpm install --fix-lockfile, including filtered installs, and regenerate broken lockfile metadata while preserving compatible locked versions pnpm/pnpm#14250.
Fixed intermittent Access is denied failures when concurrent global commands hand off the global bin lock on Windows.
Fixed the --shamefully-hoist CLI option being rejected pnpm/pnpm#14235.
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.
Installs that run no build scripts finish faster, especially in workspaces with many projects.
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.
Workspace installs are substantially faster (~0.7 s on a 60-project workspace): after hoisting, pnpm now shims only the bins of publicly hoisted workspace packages instead of re-walking every project's node_modules to rediscover bins that were already linked.
Fixed a large install-time regression on macOS for installs that rebuild node_modules from a warm store #14231. APFS serializes file-cloning and hard-linking syscalls volume-wide, so importing packages one file at a time from many threads was bounded by a per-volume ceiling and got slower the more CPU cores the machine had. On macOS, pnpm install now materializes each package once into the store's links directory (the same canonical slots enableGlobalVirtualStore uses) and copies it into node_modules/.pnpm with a single copy-on-write directory clone per package, replacing tens of thousands of per-file syscalls with one per package. Applies with the default nodeLinker: isolated when enableGlobalVirtualStore is off and packageImportMethod is auto, clone, or clone-or-copy; hoisted, global-virtual-store, and explicit hardlink/copy installs are unchanged.
Stop in-flight recursive run and exec commands when bailing after the first failure.
Warm installs that rebuild node_modules on macOS are about 10% faster: creating each package's virtual-store directory now issues fewer filesystem calls.
An _auth credential in an .npmrc now authenticates even when its base64 is written without the trailing = padding (or with extra padding, or with whitespace inside it), instead of failing with a 401. An _auth that is not valid base64, or that carries no : between the username and the password, now fails with ERR_PNPM_AUTH_INVALID_BASE64 / ERR_PNPM_AUTH_MISSING_SEPARATOR #14257.
Colored output is no longer printed as raw escape sequences in the Windows Command Prompt #14292. Commands such as pnpm list now style their output there.
Deprecated the pnpmfile filterLog hook in pnpm v12. The Rust CLI ignores it and emits a warning.
Git dependencies on known hosts (GitHub, GitLab, Bitbucket) are now treated as identities rather than transport choices. Every representation of the same repository — github:owner/repo, owner/repo, git+https://…, git+ssh://git@… — resolves through the host's canonical HTTPS URL, and the lockfile never records an SSH URL for them. Repositories whose archive endpoint is anonymously reachable resolve to the host's archive (fast tarball download); all others resolve to a git clone of the canonical HTTPS URL, which every machine with access to the repository can fetch.
To reach a private hosted repository over SSH, configure the machine (not the project) with git's own URL rewriting, for example:
git config --global url."git@github.com:".insteadOf https://github.com/
pnpm shells out to git, so the rewrite applies to all of pnpm's git operations automatically. URLs of unknown hosts (self-hosted servers) are unaffected and keep their exact URL, including SSH. URLs with embedded credentials are also kept verbatim and never resolve to a host archive.
This removes the network probing that previously decided between HTTPS and SSH at resolution time, which could record a transport that only worked on the machine that happened to run the resolution (e.g. an SSH URL that broke CI runners without SSH keys).
A project's pnpm-workspace.yaml may no longer carry a setting pnpm does not recognize. Such a setting used to be ignored in silence — a misspelled minimumReleaseAge dropped the policy it was meant to set, and nothing said so. Now it is reported, suggesting the closest real setting name when the key looks like a typo, and it fails the command with ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS when the project pins a pnpm version the running pnpm satisfies: with the pin honored, the setting cannot be meant for a different pnpm version, so it is a mistake to fix rather than a key to ignore. Everywhere else it is a warning, so a project that has yet to be cleaned up keeps working.
The pnpm config subcommands never fail on such a setting, so a broken file can still be inspected and repaired, and pnpm config get <key> prints the value with no warnings at all. Keys the global config file cannot set are likewise split between workspace-only settings (still directed to pnpm-workspace.yaml) and settings unknown to this version.
Dependency cycles are now broken canonically during peer resolution: the members of each cycle are ordered by package id, and the edges that close a cycle are always cut at the same place, no matter where the installation walks into the cycle from. Previously the cut depended on the walk path, so installing the same dependencies could produce different lockfiles depending on importer order or resolution order #13846, and a peer-resolution verdict computed for one occurrence of a cyclic package could be wrongly reused at another #13865.
With canonical cycle breaking the lockfile is a pure function of the dependency graph: repeated installs, reordered importers, and reordered dependencies all produce byte-identical lockfiles. Peer dependencies of packages inside a cycle keep nearest-wins resolution along the canonical order, and a dependency edge that closes a cycle references an occurrence of its target resolved at the importer level. On large cycle-heavy workspaces peer resolution is 2–3× faster, uses about 25% less memory, and produces a substantially smaller lockfile (fewer redundant peer variants).
Existing lockfiles keep working: headless (--frozen-lockfile) installs consume them unchanged, and installs that skip resolution leave them untouched. The first install that actually re-resolves (for example after a dependency change) re-keys walk-order-dependent peer variants of cyclic packages once.
packageImportMethod: auto now tries hardlinks before cloning on Linux. A reflink materializes a new inode and copies extent bookkeeping inside the filesystem's metadata trees, where a hardlink is one directory entry — on btrfs this roughly halves the time an install spends materializing node_modules from a warm store. ext4 installs are unchanged (cloning was never supported there, so auto already hardlinked), and macOS keeps clone-first, where APFS clonefile is the platform's cheap primitive. Cloning remains the fallback when the store refuses hardlinks, and remains available explicitly via packageImportMethod: clone.
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.
Globally installed bins can now follow the project you run them in. The new globalShims setting is a record of package names to policies that selects which globally installed packages get project-aware shims; it defaults to { node: true, deno: true, bun: true } and merges key-wise, so globalShims: { bun: false } switches one default off and globalShims: { typescript: true } adds another package. With the default, a project that pins Node.js through devEngines.runtime or engines.runtime gets the pinned stable release — authenticated against the Node.js release-team signatures — downloaded on first use and run whenever you type node inside the project, with no shell hooks. Candidates that are not signature-verified (Deno, Bun, Node.js prereleases, and ordinary package bins you enable) ask "Do you trust this project?" once per candidate and remember the answer machine-locally; the record values name the policy per package: "auto" (or its shorthand true) defers to artifact authentication, "always" switches without ever asking (useful in CI), and "prompt" always asks, even for authenticated candidates. Set globalShims: false to disable the feature, or PNPM_SHIM_BYPASS=1 to bypass it for one invocation. On Windows, programs can keep spawning the global node.exe directly, without a shell.
pnpm installs the other package managers now, not just itself: npm, Yarn Classic, Yarn Berry, Yarn 6 (yarnpkg/zpm), and Bun. Each is resolved and fetched through the trusted package-manager registries, and an npm-published one is verified against npm's signature for its exact version before it is executed.
Three things use it:
packageManager / devEngines.packageManager pin is honored, and a yarn.lock written by Yarn Classic no longer gets installed by Yarn Berry. pnpm provides that package manager when the dependency pinned a version, or when the host cannot satisfy what the dependency needs — so a repository built with Yarn now installs on a machine that has only pnpm, while a host that already has a suitable one keeps using its own.pnpm dlx (pnx) runs one of them for a single command: pnx yarn@4 install, pnx npm@11 ci, pnx bun@1.3.0 install. Naming a package manager, or a runtime (node, deno, bun), there now provisions the real thing instead of installing the npm package that shares its name — unless the specifier locates a package rather than asking for a released version (pnx yarn@npm:yarn@1.22.22, pnx yarn@yarnpkg/berry), which installs what it names — pnx yarn@4 was previously a missing version, since Yarn 4 is published as @yarnpkg/cli-dist, and pnx node@22 now runs that Node.js release rather than a wrapper that downloads one. --package naming a package manager picks which of its commands to run, so pnx --package npm@11 npx create-something runs that npm's npx.pnpm shim add yarn links a yarn command that runs whatever version the current project pins, and pnpm shim rm / pnpm shim ls manage those shims. It works for any package, not only package managers. Shims are never created as a side effect of pnpm setup or an install — a shim shadows the rest of your PATH, so pnpm only writes one when asked.Installing a package manager globally (pnpm add -g yarn) now makes it follow a project's pin too, the way a globally installed Node.js already follows devEngines.runtime: the pinned version runs where a project pins one, and the globally installed copy is the fallback everywhere else. An explicit globalShims entry, including false, is left as you set it.
pnpm add follows the same rule about what a name means. pnpm add -g yarn@4 installs Yarn Berry — it used to fail, because npm's yarn package stops at Classic — and pnpm add -g node@22 / pnpm add -g deno@2 install that Node.js or Deno release rather than a wrapper package that downloads one. In a project, naming a package manager records which one the project uses instead of installing it as a dependency, and naming a runtime records it under engines.runtime as node@runtime:22 already did.
The declaration goes where the package manager reads it. Yarn is started from a project pin by corepack, which reads only packageManager and only accepts an exact version there, so pnpm add yarn@4 resolves the line and writes "packageManager": "yarn@4.18.0" — the same thing corepack use yarn@4 writes, down to the +sha512.… integrity for the Yarn Classic line that corepack pins its tarball with. Every other package manager is recorded in devEngines.packageManager, which holds a range. Only one of the two fields is ever left behind: they declare the same thing, and corepack refuses to run a project whose declarations disagree.
A JavaScript package manager on a machine without Node.js gets a managed LTS runtime to run on.
What changes for a project coming from v11: pnpm add yarn records the project's package manager instead of installing the npm package that shares the name (that package is still reachable as pnpm add yarn@npm:yarn@1.22.22), pnpm add -g yarn installs the current Yarn line rather than Classic, pnpm add -g node / pnpm add -g deno and pnx node / pnx deno install a Node.js or Deno release rather than a wrapper package, and a globally installed package manager defers to a project's pin where there is one.
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 the audit.ignorePrune setting. When set to true, pnpm audit --fix removes ignored GHSA entries that no longer appear in the audit report.
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.
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.
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.
Running pnpm setup, pnpm self-update, or a command that modifies the global installation (such as pnpm add --global) through sudo now fails with ERR_PNPM_SUDO_NOT_SUPPORTED instead of silently operating on the root user's home directory. pnpm keeps global packages and configuration in the invoking user's home directory, so these commands never need root permissions. Read-only global commands (such as pnpm bin --global) still work under sudo.
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.
Deprecated the pnpmfile filterLog hook in pnpm v12. The Rust CLI ignores it and emits a warning.
The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages that are only imported for their types, so installing them was at best unnecessary and at worst broke the dependent: @typescript-eslint/types gained a typescript dependency resolved to the newest release, which put TypeScript 7 under older @typescript-eslint versions and made ESLint fail with "Cannot read properties of undefined (reading 'Intrinsic')". The database keeps its @yarnpkg/extensions entries and pnpm's own curated ones.
When no directory above the project accepts a hard link — inside an AI agent sandbox that only grants write access to the project, or a container with just the project mounted writable — the default store is now created at <project>/node_modules/.pnpm-store instead of in the pnpm home directory. In those environments the home store is either read-only or on another volume, which forces every package to be copied instead of hard linked #13525.
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 l
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.Under nodeLinker: isolated, a Bit root-component member whose materialized copy carries no package.json now receives sibling symlinks for the dependencies its own lockfile snapshot declares, instead of a symlink to every other member of the root. The all-member fallback remains only when no snapshot exists.
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.
Fixed pnpm v11 incorrectly reporting confirmModulesPurge as unrecognized when set in pnpm-workspace.yaml. The Rust CLI now identifies the unsupported
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.
A +<algorithm>.<hash> build in a devEngines.packageManager version no longer makes pnpm install --frozen-lockfile fail with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE on a lockfile a plain install kept rewriting identically #14124.
The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages that are only imported for their types, so installing them was at best unnecessary and at worst broke the dependent: @typescript-eslint/types gained a typescript dependency resolved to the newest release, which put TypeScript 7 under older @typescript-eslint versions and made ESLint fail with "Cannot read properties of undefined (reading 'Intrinsic')". The database keeps its @yarnpkg/extensions entries and pnpm's own curated ones.
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.
Record the pnpm version a project pins even when the install has nothing else to do. Adding a devEngines.packageManager (or packageManager) pin to a project whose dependencies are already installed left packageManagerDependencies unwritten, so pnpm install --frozen-lockfile failed with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE while a plain pnpm install reported "Already up to date" without recording it #14124.
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…
A project's pnpm-workspace.yaml may no longer carry a setting pnpm does not recognize. Such a setting used to be ignored in silence — a misspelled minimumReleaseAge dropped the policy it was meant to set, and nothing said so. Now it is reported, suggesting the closest real setting name when the key looks like a typo, and it fails the command with ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS when the project pins a pnpm version the running pnpm satisfies: with the pin honored, the setting cannot be meant for a different pnpm version, so it is a mistake to fix rather than a key to ignore. Everywhere else it is a warning, so a project that has yet to be cleaned up keeps working.
The pnpm config subcommands never fail on such a setting, so a broken file can still be inspected and repaired, and pnpm config get <key> prints the value with no warnings at all. Keys the global config file cannot set are likewise split between workspace-only settings (still directed to pnpm-workspace.yaml) and settings unknown to this version.
Added global build approvals pnpm/pnpm#14101.
Added recursive global outdated checks pnpm/pnpm#14101.
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.Added support for configuring stateDir in the Rust pnpm CLI pnpm/pnpm#12042.
Added bounded workspace concurrency for recursive run and exec commands pnpm/pnpm#14101.
@pnpm/napi gained reporter output, reverse dependency queries, and lockfile access.
install and rebuild accept options.reporter and render pnpm's terminal output — progress line, packages-diff summary, lifecycle output, and the Done in … footer. Rendered output goes to stdout, or to an onOutput callback for a host that writes its own output through JavaScript. New reporting options: hideLifecycleOutput, ignoredBuildsInstructionText, and hideLinkedPkgsDiff.
getDependents returns the reverse dependency trees behind pnpm why, annotated with the package.json fields named in manifestFields. renderDependents returns those trees rendered as tree, parseable, or JSON output.
readLockfile and writeLockfile read and write pnpm-lock.yaml (or the current lockfile under the virtual store). filterLockfileByImporters returns a lockfile narrowed to what the named importers reach. readModulesManifest returns the .modules.yaml state of an installed node_modules.
Top-level lockfile keys pnpm does not define are no longer dropped when a lockfile is loaded and saved, so state a tool records beside pnpm's own keys survives a rewrite.
pnpm now supports per-branch lockfiles in its Rust engine:
gitBranchLockfile gives each git branch its own pnpm-lock.<branch>.yaml, so two branches can hold different resolutions without conflicting on one file. A branch that has no lockfile yet installs against the shared pnpm-lock.yaml.mergeGitBranchLockfiles (and the --merge-git-branch-lockfiles flag on pnpm install) folds every branch lockfile back into pnpm-lock.yaml and deletes them, which is what merging a branch into the mainline needs.mergeGitBranchLockfilesBranchPattern (and --merge-git-branch-lockfiles-branch-pattern) names the branches that merge automatically, so a mainline branch does not have to pass the flag by hand #12042.Added PNPM_CONFIG_VIRTUAL_STORE_ONLY and PNPM_CONFIG_ENABLE_MODULES_DIR support to the Rust pnpm CLI.
Added support for the lockfileDir setting and its --lockfile-dir <dir> flag on pnpm install, add, update, and remove. pnpm-lock.yaml, the root node_modules holding the virtual store, and the config dependencies now live in the given directory, each project is recorded under its path relative to it, and every project keeps its own node_modules of symlinks — so several projects can share one lockfile #12042.
Added support for the preferSymlinkedExecutables setting. On POSIX systems, node_modules/.bin entries are created as symlinks to the executable files instead of shell shims, and NODE_PATH pointing at the virtual store of the workspace root is exported to spawned scripts so they can resolve dependencies from the hoisted store. Like the TypeScript CLI, the setting turns on automatically when nodeLinker is set to hoisted.
Added the six CLI flags the TypeScript pnpm CLI accepts but the Rust CLI did not #14101:
--stream prints a recursive command's script output as it arrives, one line at a time, prefixed with the project it came from, instead of letting the scripts write to the terminal directly. --parallel implies it, as in pnpm.--aggregate-output holds each script's streamed output until the script exits and then prints it as one block, so concurrent projects can't interleave.--reporter-hide-prefix drops that project prefix from the scripts' own output lines. On a recursive pnpm exec, the opposite spelling --no-reporter-hide-prefix turns the prefixing on.--use-stderr sends the reporter's output to stderr, leaving stdout for the command's own result.--ignore-workspace runs the command as if the project were standalone: no workspace root is discovered, so pnpm-workspace.yaml contributes neither settings nor sibling projects, and a blocked dependency build is not scaffolded into its allowBuilds.--workspace-packages overrides the packages patterns of pnpm-workspace.yaml for the run.The stream, aggregateOutput, reporterHidePrefix, useStderr, and ignoreWorkspace settings are now read from pnpm-workspace.yaml, the global config.yaml, and their PNPM_CONFIG_* environment variables too.
Added support for the shellEmulator setting. With it enabled, the scripts pnpm run executes, a project's own lifecycle scripts, and dependencies' build scripts run in a built-in POSIX shell instead of the platform's (sh -c, or cmd /d /s /c on Windows), so scripts written for sh behave the same on every OS. scriptShell is not used while the emulator is on.
The Rust engine now checks that a package read back from the store is the package it was recorded as. When the tarball's package.json names a different name or version than the store entry was keyed for — a broken lockfile, or a registry serving content that doesn't match its metadata — the install fails with ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE. Set the new strictStorePkgContentCheck setting to false to downgrade the failure to a warning and install from the entry anyway #12042.
pnpm now supports three workspace settings in its Rust engine:
includeWorkspaceRoot (and the universal --include-workspace-root / --no-include-workspace-root flags) keeps the workspace root project in a recursive run, exec, add, or test, which otherwise leave it out.ignoreWorkspaceCycles and disallowWorkspaceCycles control the report an install makes when workspace projects depend on each other in a cycle: it is a warning by default, an ERR_PNPM_DISALLOW_WORKSPACE_CYCLES error under disallowWorkspaceCycles, and silent under ignoreWorkspaceCycles #12042.Added support for the remaining pnpm default settings, including recursive command controls, optional dependency selection, workspace-root checks, color modes, lockfile compatibility, and pack manifest options.
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.
Added the commands the Rust CLI was still missing:
pnpm get <key> and pnpm set <key> <value> — the top-level spellings of pnpm config get and pnpm config set.pnpm store status — reports the packages whose files no longer match the store they were expanded from, failing with ERR_PNPM_MODIFIED_DEPENDENCY; and pnpm store add <pkg>... — fetches packages into the store without writing a manifest, a lockfile, or node_modules.pnpm env use --global <version> and pnpm env list [<selector>], the deprecated Node.js-only front end to pnpm runtime.pnpm edit, pnpm profile, pnpm token, and pnpm xmas now fail with ERR_PNPM_NOT_IMPLEMENTED pointing at the npm CLI, instead of being taken for a package script.An install that resolves the dependency graph now reports the unmet peer dependencies it leaves behind, matching the TypeScript CLI. By default it warns once — Issues with peer dependencies found. Run "pnpm peers check" to list them. — and with strictPeerDependencies it fails with ERR_PNPM_PEER_DEP_ISSUES after the artifacts are written, listing every unmet peer. This covers pnpm install, add, remove, update and --lockfile-only; pnpm dedupe reported the same verdict already, and now shares the reporting with them. peerDependencyRules are applied before the verdict, so a rule that covers every issue leaves nothing to report, and a --filtered install reports only on the projects it installed. An install that skips resolution — a frozen install, or one whose pnpm-lock.yaml is already up to date — reports nothing, as in the TypeScript CLI; pnpm peers check inspects such a tree #14098.
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.
Added filtered and split SBOM generation with per-project lockfiles, including reachable workspace projects and incomplete-graph validation pnpm/pnpm#14101.
Kept pending build approvals available after removing an unrelated dependency.
Fixed resolving the chcp command on Windows during pnpm setup by looking for chcp.com before chcp pnpm/pnpm#13991.
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.
pnpm dedupe accepts the pnpm install options that pnpm documents for it — --lockfile-only, --ignore-scripts, --offline, and --prefer-offline — instead of rejecting them with unexpected argument. Without --lockfile-only, pnpm dedupe now also updates node_modules, as an install does #14107.
pnpm dedupe in the Rust engine now fails with ERR_PNPM_PEER_DEP_ISSUES when strictPeerDependencies is set and unresolved peer dependency issues remain after deduplication, matching the TypeScript CLI #14099. Previously it only ever printed a warning, regardless of the setting.
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.
pnpm install --dev and pnpm deploy --dev no longer install optional dependencies, and --prod now takes precedence when combined with --dev, matching the TypeScript pnpm CLI.
A dependency published with "bin": "", such as url-loader@1.1.2, no longer fails the install with ERR_PNPM_CMD_SHIM_PROBE_SHIM_SOURCE #13962. An empty bin declares no command, as it does in pnpm v11, so no shim is written for the package; a directories.bin entry on the same package is still linked.
A dependency pinned to an exact version carrying semver build metadata ("@parcel/codeframe": "2.0.0-canary.1718+d8408010f") installs again instead of failing with ERR_PNPM_NO_MATCHING_VERSION #14096. npm strips build metadata when it publishes a version, so pnpm strips it from the version it looks up, matching npm and pnpm v11.
A package's files entries now match only at the package root, the way npm reads them. A bare src used to also match nested directories such as example/src, so a dependency installed from git could ship the repository's own example app. The same filter decides what pnpm pack and pnpm publish put in a tarball and what pnpm deploy copies, so those stop carrying the extra files too. Exclusions such as !**/__tests__ and !*.map still match at any depth. A package already in the store keeps its old file set until it is fetched again.
A pnpm install --filter <selector> run that has nothing to do now reports "Already up to date" without entering the install pipeline, the same way an unfiltered pnpm install already did #14033.
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.
Settings written to a pnpm-workspace.yaml block that uses inline (flow) YAML — catalog: { foo: ^1.0.0 }, overrides: { foo: 1.0.0 }, minimumReleaseAgeExclude: [foo@1.0.0] — are now edited in place instead of failing or corrupting the file. pnpm audit, pnpm link, pnpm approve-builds, pnpm patch, pnpm add --config, and catalog updates all keep the block's flow style, its other entries, and its comments #14108.
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.
When a git-hosted dependency is blocked from running build scripts, the error now suggests an allowBuilds entry that actually approves it. It quoted the bare package name, which never matches a git-hosted package, so following the suggestion left the install failing the same way #14002.
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.
Added support for the globalPnpmfile setting, which names a user-level pnpmfile that runs for every project ahead of the project's own. Like pnpm, it is left out of the lockfile's pnpmfileChecksum, so editing it does not decide whether a lockfile is still current. pnpmfile and globalPnpmfile are now also readable from PNPM_CONFIG_PNPMFILE and PNPM_CONFIG_GLOBAL_PNPMFILE.
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.
A repeat pnpm install with nodeLinker: hoisted is a no-op again when a workspace package declares the dependencies #14001. The hoisted linker installs them into the root node_modules, but the up-to-date check previously looked under each package's own node_modules and reinstalled the whole tree every time. A hoisted install also no longer reports the packages it just wrote as broken.
ignorePnpmfile can now be set in pnpm-workspace.yaml and read from PNPM_CONFIG_IGNORE_PNPMFILE, not only passed as --ignore-pnpmfile, so a project or a machine can turn pnpmfile hooks off once instead of adding the flag to every command. The flag still applies on top. As in pnpm, the global config.yaml cannot set it: a pnpmfile belongs to the project that ships it.
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.
--config.minimum-release-age is honored again, along with --config.minimum-release-age-exclude, --config.minimum-release-age-ignore-missing-time and --config.minimum-release-age-strict. Each overrides the matching pnpm-workspace.yaml setting, and the exclude flag may be repeated to build a list #13929.
An unreadable node_modules/.modules.yaml no longer makes pnpm install delete node_modules and relink every package on each run. The unparsable state file is now reported as an error instead #14062.
pnpm outdated and pnpm update --interactive now leave out the dependencies listed in updateConfig.ignoreDependencies, instead of reporting them and offering them for update.
Fixed pnpm outdated and pnpm update --interactive offering versions blocked by minimumReleaseAge pnpm/pnpm#14004.
pnpm pack writes tar entries in the POSIX ustar header form npm uses — ustar\0 magic and the explicit 0 regular-file typeflag — instead of the GNU form with a NUL typeflag, which strict tar readers such as publint mistake for the end-of-archive marker #13924.
Fixed --config.ignore-scripts=true not being honored by CLI commands such as pnpm pack #13986.
pnpm install <pkg> now adds the package, the same as pnpm add <pkg> and matching the JavaScript CLI. It previously ended in a usage error: pnpm i valibot printed error: unexpected argument 'valibot' found instead of saving the dependency #13886.
Fixed Plug'n'Play projects to preload .pnp.cjs for dependency and project lifecycle scripts, pnpm run, and pnpm exec. The generated loader now also exposes the public Yarn PnP API surface.
Workspace packages declared with a parent-relative pattern in pnpm-workspace.yaml (../shared, ../../docs/*) are discovered again. They were dropped from the project list, so pnpm list -r and --filter did not see them and a frozen install of a lockfile that already held their importer entries failed with ERR_PNPM_PACKAGE_MANAGER_UNSAFE_IMPORTER_PATH.
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 path named by the pnpmfile setting that is not on disk now fails with ERR_PNPM_PNPMFILE_NOT_FOUND and names the file, instead of surfacing as a generic pnpmfile execution failure. Discovery of the default .pnpmfile.mjs / .pnpmfile.cjs is unaffected: a project that ships neither still installs normally.
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.
Suggest pnpm shim add <runtime> after pinning a project runtime when no project-aware global shim is installed. Explicit project-aware shims now reject unrelated global bin conflicts and are restored after a matching global package is removed or replaced by a version that drops its bin.
pnpm -r update --latest --depth 0 <selector> now fails with ERR_PNPM_NO_PACKAGE_IN_DEPENDENCIES when no project in the workspace declares a matching dependency, instead of silently doing nothing.
Fixed repeat installs paying for a full lockfile comparison forever after a modification-time collision. When a package.json was last modified inside the same clock tick that the install recorded as its validation baseline — a fast install, a checkout that copied files with identical timestamps, or any filesystem that keeps only whole-second modification times — the manifest kept reading as possibly-modified, so every later pnpm install and verify-deps-before-run check re-compared the manifests against the lockfile instead of taking the fast path #13907.
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 sbom now honours --filter-prod, the full --filter selector syntax (dependency queries such as pkg..., {dir} and glob paths, [since] change queries, exclusions), and --workspace-root. Selectors that match no project print No projects matched the filters and write no SBOM, and --split emits its per-project SBOMs in a stable order.
The universal --fail-if-no-match flag is supported too: any filtered command whose selectors match no workspace project now exits with code 1 #14064.
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 update now preserves the existing range operator when updating a prerelease dependency. See pnpm/pnpm#7002.
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>@<tag> now saves the version the dist tag resolved to in package.json, keeping the range operator the dependency already declared, instead of saving the tag itself. A dependency declared through a catalog: reference, a workspace: or npm: alias, or a path or git specifier keeps its declaration, and one that already tracks a dist tag records the tag asked for #14092.
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.
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.
On Windows, pnpm store path now returns a conventional drive path without the \\?\ verbatim prefix when the project and pnpm home are on different drives #13987.
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) i…
packageImportMethod: auto now tries hardlinks before cloning on Linux. A reflink materializes a new inode and copies extent bookkeeping inside the filesystem's metadata trees, where a hardlink is one directory entry — on btrfs this roughly halves the time an install spends materializing node_modules from a warm store. ext4 installs are unchanged (cloning was never supported there, so auto already hardlinked), and macOS keeps clone-first, where APFS clonefile is the platform's cheap primitive. Cloning remains the fallback when the store refuses hardlinks, and remains available explicitly via packageImportMethod: clone.
This ships with pnpm 12 only: pnpm 11's importer deliberately keeps clone-first, since changing what the default materializes on disk is not a point-release change.
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".
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.
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.
Reduced warm update overhead by limiting virtual-store bin linking and ignored-script build bookkeeping to packages materialized by the current install.
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.
node-linker=hoisted installs no longer produce broken layouts on graphs with version conflicts. Three hoister fixes, aligning with @yarnpkg/nm (which the TypeScript CLI delegates to):
parse-entities@4 resolving character-entities-legacy v1 instead of v3, which crashes with ERR_IMPORT_ATTRIBUTE_MISSING on Node.js 22. Hoist decisions are now made per parent path on decoupled copies (ports upstream's decoupleGraphNode).depPathByPkgId mapping) instead of conflict-nesting a copy under every dependent — on peer-variant-heavy graphs (such as bit's) the old behavior also made the per-path walk explode.usedDependencies gate).pnpm update --no-save <pkg>@<version> now keeps the manifest's declared importer specifier in pnpm-lock.yaml when the requested version satisfies that range, so a subsequent --frozen-lockfile install no longer fails because the lockfile records the requested version as the specifier.
Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win pnpm/pnpm#13976.
Improved install performance: the store-index writer's shutdown now overlaps the install's final lockfile and .modules.yaml writes instead of extending the install's tail.
A setting in the global config.yaml that pnpm does not read from that file, or that is written in kebab-case instead of camelCase, is now reported instead of being ignored silently.
A forced full re-resolution (config changes the fast lockfile update cannot absorb, such as a changed override or packageExtensions) no longer moves dependencies whose recorded versions still satisfy their ranges. The prior lockfile now pins each still-satisfied edge even when its recorded subtree cannot be reused wholesale, so open ranges like @types/node: "*" keep their locked versions instead of collapsing onto the highest locked version and churning the lockfile.
Improved fresh resolution performance when package metadata is already cached.
Improved fresh installs by reusing the store index and verified-files cache during dependency materialization.
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 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.
Improved install performance: large tarballs are now verified and extracted while they download, so the biggest packages — whose downloads finish last — no longer add their whole extraction to the end of the install.
namedRegistries is deprecated in favor of the prefix field, and is still read for prefixes registries does not declare.
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.
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.
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.
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.
Fixed pnpm patch-commit in project and edit paths containing non-ASCII characters.
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 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.
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.
pnpm outdated and pnpm update --interactive now dereference catalog: specifiers before querying the registry. A catalog entry that is an npm alias ('@types/zkochan__table': npm:@types/table@6.3.2) no longer fails with ERR_PNPM_OUTDATED_REGISTRY_ERROR for the alias key, and pnpm outdated --compatible compares against the range the catalog holds instead of skipping the dependency.
A failed packument request now reports the status the registry returned (404 Not Found) instead of "error decoding response body".
Installs with a cold cache are significantly faster: lockfile verification no longer delays resolution or downloads and re-checks far less data over the network, and downloaded packages are linked while the remaining downloads are still in flight.
Fixed pnpm installs using pnpr to honor the client's autoInstallPeers, dedupePeers, and excludeLinksFromLockfile settings pnpm/pnpm#13389.
The three registry lookups are now named for what they are keyed by, so that none of them is called registries — a name the registries setting itself has taken:
| before | after |
|---|---|
Config.registries |
Config.registriesByScope |
Config.namedRegistries |
Config.registriesByPrefix |
Config.registryOptions |
Config.registryOptionsByUrl |
The same rename applies to the RegistryContext fields, the Registries and NamedRegistries types (now RegistriesByScope and RegistriesByPrefix), normalizeRegistries / normalizeNamedRegistries (now normalizeRegistriesByScope / normalizeRegistriesByPrefix), and the BUILTIN_NAMED_REGISTRIES constant (now BUILTIN_REGISTRIES_BY_PREFIX).
This is an internal rename: no setting, error code, lockfile field, or .pnpmfile.cjs hook field changes. A preResolution hook still reads ctx.registries, which is the name pacquet passes as well. The registries and namedRegistries settings are read under the names users write them.
The pnpr resolve request sends registriesByPrefix where it sent namedRegistries. A pnpr server and its clients must be on matching versions, which is already the case for an experimental server.
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.
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.
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).
Reduced peak memory usage when installing large packages. A tarball whose compressed size is at least 16 MiB, or whose registry-reported unpacked size is at least 64 MiB, is now extracted by streaming the decompression directly into the content-addressable store instead of materializing the whole decompressed archive in memory, and its large files are hashed and written to the store incrementally.
pnpm why and pnpm list no longer print stray [90m-style codes in their trees when the terminal supports colors. The bolded labels — the searched package in pnpm why, the project header and the matched package in pnpm list — dropped the escape byte of the styles they already carried, leaving the color codes as visible text.
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 lockf
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.
pnpm installs the other package managers now, not just itself: npm, Yarn Classic, Yarn Berry, Yarn 6 (yarnpkg/zpm), and Bun. Each is resolved and fetched through the trusted package-manager registries, and an npm-published one is verified against npm's signature for its exact version before it is executed.
Three things use it:
packageManager / devEngines.packageManager pin is honored, and a yarn.lock written by Yarn Classic no longer gets installed by Yarn Berry. pnpm provides that package manager when the dependency pinned a version, or when the host cannot satisfy what the dependency needs — so a repository built with Yarn now installs on a machine that has only pnpm, while a host that already has a suitable one keeps using its own.pnpm dlx (pnx) runs one of them for a single command: pnx yarn@4 install, pnx npm@11 ci, pnx bun@1.3.0 install. Naming a package manager, or a runtime (node, deno, bun), there now provisions the real thing instead of installing the npm package that shares its name — unless the specifier locates a package rather than asking for a released version (pnx yarn@npm:yarn@1.22.22, pnx yarn@yarnpkg/berry), which installs what it names — pnx yarn@4 was previously a missing version, since Yarn 4 is published as @yarnpkg/cli-dist, and pnx node@22 now runs that Node.js release rather than a wrapper that downloads one. --package naming a package manager picks which of its commands to run, so pnx --package npm@11 npx create-something runs that npm's npx.pnpm shim add yarn links a yarn command that runs whatever version the current project pins, and pnpm shim rm / pnpm shim ls manage those shims. It works for any package, not only package managers. Shims are never created as a side effect of pnpm setup or an install — a shim shadows the rest of your PATH, so pnpm only writes one when asked.Installing a package manager globally (pnpm add -g yarn) now makes it follow a project's pin too, the way a globally installed Node.js already follows devEngines.runtime: the pinned version runs where a project pins one, and the globally installed copy is the fallback everywhere else. An explicit globalShims entry, including false, is left as you set it.
pnpm add follows the same rule about what a name means. pnpm add -g yarn@4 installs Yarn Berry — it used to fail, because npm's yarn package stops at Classic — and pnpm add -g node@22 / pnpm add -g deno@2 install that Node.js or Deno release rather than a wrapper package that downloads one. In a project, naming a package manager records which one the project uses instead of installing it as a dependency, and naming a runtime records it under engines.runtime as node@runtime:22 already did.
The declaration goes where the package manager reads it. Yarn is started from a project pin by corepack, which reads only packageManager and only accepts an exact version there, so pnpm add yarn@4 resolves the line and writes "packageManager": "yarn@4.18.0" — the same thing corepack use yarn@4 writes, down to the +sha512.… integrity for the Yarn Classic line that corepack pins its tarball with. Every other package manager is recorded in devEngines.packageManager, which holds a range. Only one of the two fields is ever left behind: they declare the same thing, and corepack refuses to run a project whose declarations disagree.
A JavaScript package manager on a machine without Node.js gets a managed LTS runtime to run on.
What changes for a project coming from v11: pnpm add yarn records the project's package manager instead of installing the npm package that shares the name (that package is still reachable as pnpm add yarn@npm:yarn@1.22.22), pnpm add -g yarn installs the current Yarn line rather than Classic, pnpm add -g node / pnpm add -g deno and pnx node / pnx deno install a Node.js or Deno release rather than a wrapper package, and a globally installed package manager defers to a project's pin where there is one.
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.
Corepack can run pnpm 12 again #13018. Corepack installs no dependencies and runs no lifecycle scripts, so the native binary that the pnpm package normally receives from its platform-specific optional dependency was never there, and corepack use pnpm@next-12 failed with MODULE_NOT_FOUND. The package now ships the bin/pnpm.mjs and bin/pnpx.mjs entry points Corepack looks for; they fetch the pinned native binary on first use — verified against npm's signature and checksum, honouring COREPACK_NPM_REGISTRY and the rest of Corepack's registry environment — and hand over to it. Installing pnpm with a package manager is unaffected and still runs the binary directly, with no Node.js startup in between.
With a configured pnprServer, pnpm install skips the server exchanges it does not need, closing the gap where an up-to-date project paid a full resolve round trip that a direct install answered locally pnpm/pnpm#13904:
pnpm-lock.yaml still satisfies every manifest skips the server resolve exchange and materializes node_modules from the on-disk lockfile.lockfile-verified.jsonl cache already covers the lockfile under the current policy; server-verified and server-resolved lockfiles are now recorded into that cache.trustPolicy*, minimumReleaseAgeStrict, or minimumReleaseAgeExclude settings now invalidates the repeat-install fast path, matching the TypeScript CLI's workspace-state check.The published packages now ship a THIRD-PARTY-NOTICES.md file carrying the BSD 2-Clause license of the Yarn code that pnpm's hoisted-layout algorithm and built-in package-compatibility database are derived from.
Breaking change. Dependency cycles are now broken canonically during peer resolution: the members of each cycle are ordered by package id, and the edg…
Breaking change. Dependency cycles are now broken canonically during peer resolution: the members of each cycle are ordered by package id, and the edges that close a cycle are always cut at the same place, no matter where the installation walks into the cycle from. Previously the cut depended on the walk path, so installing the same dependencies could produce different lockfiles depending on importer order or resolution order #13846, and a peer-resolution verdict computed for one occurrence of a cyclic package could be wrongly reused at another #13865.
With canonical cycle breaking the lockfile is a pure function of the dependency graph: repeated installs, reordered importers, and reordered dependencies all produce byte-identical lockfiles. Peer dependencies of packages inside a cycle keep nearest-wins resolution along the canonical order, and a dependency edge that closes a cycle references an occurrence of its target resolved at the importer level. On large cycle-heavy workspaces peer resolution is 2–3× faster, uses about 25% less memory, and produces a substantially smaller lockfile (fewer redundant peer variants).
Existing lockfiles keep working: headless (--frozen-lockfile) installs consume them unchanged, and installs that skip resolution leave them untouched. The first install that actually re-resolves (for example after a dependency change) re-keys walk-order-dependent peer variants of cyclic packages once.
Auto-installed peer dependencies are no longer resolved to their lowest satisfying versions under resolutionMode: lowest-direct or time-based. A hoisted peer is not a dependency the project declares, so it resolves like a transitive dependency — to the highest version satisfying the peer range (under time-based, the highest within the publish-date cutoff) #13871.
The resolved dependency graph and lockfile no longer depend on the order in which workspace projects are listed or discovered: importers are processed in project-id order, so reordering the packages globs in pnpm-workspace.yaml (or any other change to project listing order) produces a byte-identical lockfile #13846. This also makes auto-installed peer placement, deprecation-warning attribution, and cycle back-edge bindings a function of the project set alone.
Fixed non-deterministic lockfiles on cold installs of projects with cyclic peer dependencies: resolved peer variants could silently drop from the lockfile depending on traversal order #13846, #13865.
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 update now writes the new version range back to package.json (and to the catalog: entry a dependency points at), instead of only updating the lockfile #13879. The range operator the dependency already declared is preserved, and a dependency declared through a dist-tag ("foo": "latest") keeps tracking the tag under both pnpm update and pnpm update --latest.
Added a new setting minimumReleaseAgeExcludePrune. When enabled, pnpm add, pnpm update, and pnpm remove prune the entries of minimumReleaseAgeExclude
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.
Added support for the syncInjectedDepsAfterScripts setting. It names the scripts after which every injected copy of the package that ran them is brought back in step with its source, so a build script no longer leaves the copies in the virtual store holding stale files.
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.
Bounded the number of requests in flight to the .pnpmfile.cjs worker process. An install that runs the readPackage hook for thousands of packages at once no longer risks failing with ERR_PNPM_PNPMFILE_FAIL on a hook timeout spent waiting in the queue rather than running the hook, and holds fewer copies of the manifests it is hooking while it waits.
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.
Fixed pnpm install in CI to use frozen lockfile mode by default when an existing pnpm-lock.yaml is non-empty. An outdated lockfile now fails without being rewritten, while projects without a lockfile or with an empty lockfile can still generate one.
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.
Resolving peer dependencies in a workspace whose dependency graph contains many peer-dependency cycles now needs less than half the memory and finishes about twice as fast. Verdicts computed inside dependency cycles are now cached and reused for the occurrences they are provably valid for, instead of being recomputed for every occurrence.
pnpm install and pnpm dedupe no longer eat all the available memory while resolving a graph in which many packages declare the same missing peer dependency, such as the react peer the @radix-ui packages share #13786.
With dedupeDirectDeps, a project's symlink that becomes redundant — because the workspace root started providing the same dependency at the same resolution — is removed on the next install instead of surviving forever #13775. The layout no longer depends on install history: an incremental install now ends up with the same node_modules a clean install of the same manifests produces.
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.
pnpm --filter . deploy deploys the project in the current directory instead of the projects nested under it, so deploying the workspace root now copies the root project and installs its workspace dependencies #13758. pnpm deploy --legacy no longer rewrites the source workspace's pnpm-lock.yaml.
Fixed pnpm install writing a different pnpm-lock.yaml for an unchanged project depending on the order its dependencies happened to resolve in, which showed up as spurious lockfile diffs between installs.
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.
--frozen-lockfile no longer rejects a lockfile pnpm just generated when packageExtensions adds a peer dependency to a workspace project. The peer is auto-installed and recorded in the importer entry, but the freshness check compared against the package.json on disk, which has no such peer, and reported the entry as a removed dependency #13836.
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.
--ignore-pnpmfile is accepted again, on every command pnpm takes it on: install, add, update, dedupe, fetch, unlink, deploy, ci, and install-test #13808. The flag skips every pnpmfile hook the command would otherwise run: neither the workspace .pnpmfile.cjs nor the pnpmfiles of config dependencies are loaded, so no readPackage, updateConfig, afterAllResolved, custom resolver, or custom fetcher runs.
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.
Fixed dependency resolution letting the order in which concurrent resolutions finished decide the outcome. When one package was reached from several places, whichever occurrence got there first decided the versions its dependencies were recorded at, so repeated installs of the same project could produce different pnpm-lock.yaml files.
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.
The lockfile's time: section is no longer dropped when pnpm-lock.yaml is rewritten. resolutionMode: time-based records each direct dependency's publish date there and now reads it back as the fallback for a package whose registry metadata carries no publish date, so a later resolution derives the same cutoff instead of picking different subdependency versions #13776.
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.
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.
pnpm add <pkg>@<version> and pnpm update <pkg>@<version> now move a 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.
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.
Reduced peak memory usage while resolving peer dependencies. Workspaces with large, deeply peer-dependent dependency graphs could need gigabytes to install; the same install now needs meaningfully less.
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.
pnpm install no longer re-resolves dependencies inside a subtree the lockfile pinned when another dependency reaches the same package. Those packages kept their locked versions in node_modules while pnpm-lock.yaml recorded newer ones, so an install could quietly move a transitive dependency — including across a major version — without anything asking it to.
A .pnpmfile.cjs readPackage hook that rewrites one of a project's own dependency specifiers is now honored: rewriting "is-positive": "^1.0.0" to 1.0.0 installs 1.0.0 and records specifier: 1.0.0 for the importer. Previously the hook was applied only to the manifests of resolved dependencies, so a project's own specifier resolved against the raw range from package.json #13769.
pnpm prune now prints the Scope: all N workspace projects line when run inside a workspace, as it prunes every project of the workspace.
Removing a package from a workspace now drops its importer entry from pnpm-lock.yaml, along with the dependencies only it needed. Previously the entry survived every later install, which kept those dependencies reachable and made the lockfile diverge from the one the TypeScript CLI writes #13783.
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.
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.
Kept unselected workspace link targets shallow during filtered isolated installs.
Reduced peak memory usage while resolving peer dependencies further: each occurrence in the dependency tree now shares its package id with the edge it came from instead of owning a copy of it.
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.
Commands in a project that pins a pnpm version no longer read the whole pnpm-lock.yaml to get at the leading env document. Reading stops at the end of that document, so the cost no longer grows with the rest of the lockfile: reading the env document out of an 8 MB lockfile takes ~15µs instead of ~390µs.
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.
Command shims on POSIX again exec a target that has no interpreter — a runtime binary such as the managed Node.js, or any bin without a shebang — inst
exec a target that has no interpreter — a runtime binary such as the managed Node.js, or any bin without a shebang — instead of waiting on it. A shim that waited reported a target killed by a signal as exit code 128+N (for example 137 for SIGKILL), so callers that distinguish a signal death from an exit code, such as CI runners and process supervisors, saw the wrong outcome.Globally installed bins can now follow the project you run them in. The new globalShims setting is a record of package names to policies that selects
globalShims setting is a record of package names to policies that selects which globally installed packages get project-aware shims; it defaults to { node: true, deno: true, bun: true } and merges key-wise, so globalShims: { bun: false } switches one default off and globalShims: { typescript: true } adds another package. With the default, a project that pins Node.js through devEngines.runtime or engines.runtime gets the pinned stable release — authenticated against the Node.js release-team signatures — downloaded on first use and run whenever you type node inside the project, with no shell hooks. Candidates that are not signature-verified (Deno, Bun, Node.js prereleases, and ordinary package bins you enable) ask "Do you trust this project?" once per candidate and remember the answer machine-locally; the record values name the policy per package: "auto" (or its shorthand true) defers to artifact authentication, "always" switches without ever asking (useful in CI), and "prompt" always asks, even for authenticated candidates. Set globalShims: false to disable the feature, or PNPM_SHIM_BYPASS=1 to bypass it for one invocation. On Windows, programs can keep spawning the global node.exe directly, without a shell.pnpm dlx and pnpm create no longer fail with "Failed to read patch file" in a project that has patchedDependencies. As in pnpm, the package dlx runs is installed unpatched.
Reduced the warm startup overhead of project-aware managed runtime shims.
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.
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 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".
Git dependencies on known hosts (GitHub, GitLab, Bitbucket) are now treated as identities rather than transport choices. Every representation of the s
Git dependencies on known hosts (GitHub, GitLab, Bitbucket) are now treated as identities rather than transport choices. Every representation of the same repository — github:owner/repo, owner/repo, git+https://…, git+ssh://git@… — resolves through the host's canonical HTTPS URL, and the lockfile never records an SSH URL for them. Repositories whose archive endpoint is anonymously reachable resolve to the host's archive (fast tarball download); all others resolve to a git clone of the canonical HTTPS URL, which every machine with access to the repository can fetch.
To reach a private hosted repository over SSH, configure the machine (not the project) with git's own URL rewriting, for example:
git config --global url."git@github.com:".insteadOf https://github.com/
pnpm shells out to git, so the rewrite applies to all of pnpm's git operations automatically. URLs of unknown hosts (self-hosted servers) are unaffected and keep their exact URL, including SSH. URLs with embedded credentials are also kept verbatim and never resolve to a host archive.
This removes the network probing that previously decided between HTTPS and SSH at resolution time, which could record a transport that only worked on the machine that happened to run the resolution (e.g. an SSH URL that broke CI runners without SSH keys).
Added interactive group selection to pnpm update --global --interactive.
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.
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.
Dependencies declared with an empty version range ("adler-32": "") install again instead of failing with ERR_PNPM_NO_MATCHING_VERSION #13673. An omitted range means "any version", as it does in npm and pnpm v11, so packages that publish one — such as js-xlsx, codepage, and ssf — no longer need an overrides entry to install.
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 installs under enableGlobalVirtualStore failing with failed to remove existing directory ... prior to swap: Directory not empty (or No such file or directory) when peer variants of an injected file: dependency hash to the same slot. The link pass now materializes each unique slot directory once instead of racing one force-mode import per peer variant against the same path.
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.
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.
pnpm install again records immature versions picked under minimumReleaseAge (when minimumReleaseAgeStrict is off) in minimumReleaseAgeExclude in pnpm-workspace.yaml, so a later frozen install of the same lockfile passes verification #13687.
Reduced peak install memory: cached registry metadata is now read on demand from the on-disk metadata cache instead of being held in memory for the whole resolution. Resolving a large peer-heavy graph (@teambit/bit) peaks at about 1.3 GB instead of 3.2 GB, and a full cold install of it stays under 2 GB #13681.
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.
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.
pnpm self-update no longer fails with the installed pnpm wrapper is missing when the global packages directory carries a pnpm-workspace.yaml of global settings (written there when a global install persists an allowBuilds decision). The engine install stays anchored to its own install directory instead of walking up and adopting that file as its workspace root. The pnpm dlx cache install gets the same anchoring, so a stray pnpm-workspace.yaml above the cache directory can no longer break it #13697.
Reduced peak memory usage and allocation churn during peer dependency resolution on workspaces with many peer-dependency issue occurrences #13681.
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.
On Windows, installation no longer fails with "A required privilege is not held by the client. (os error 1314)" when symlink creation requires elevation (e.g. Developer Mode is off) — pnpm now falls back to NTFS junctions in that case. Additionally, pnpm clean and pnpm deploy --force no longer fail with "Access is denied. (os error 5)" when removing the package links inside node_modules #13694.
Running pnpm setup, pnpm self-update, or a command that modifies the global installation (such as pnpm add --global) through sudo now fails with ERR_P
pnpm setup, pnpm self-update, or a command that modifies the global installation (such as pnpm add --global) through sudo now fails with ERR_PNPM_SUDO_NOT_SUPPORTED instead of silently operating on the root user's home directory. pnpm keeps global packages and configuration in the invoking user's home directory, so these commands never need root permissions. Read-only global commands (such as pnpm bin --global) still work under sudo.Archive entries whose paths use \ as a separator are now read the same way pnpm reads them. A nested path spelled bin\tool.js by Windows publishing tooling resolves to bin/tool.js, and a path traversal spelled with backslashes is rejected instead of being stored verbatim.
Fixed file: dependencies not being re-copied when their source directory changed. A file: dependency is copied into the store at install time rather than symlinked, so editing the local package's files and running pnpm install again left the previous copy in place — the lockfile is unchanged by such an edit, so the install treated the tree as up to date.
Write blocked-build approval scaffolding to the discovered workspace manifest when using per-project lockfiles.
Concurrent installs sharing a global virtual store no longer fail with failed to remove existing directory ... prior to swap: Directory not empty, and no longer briefly remove a package directory another process is reading.
Fixed link: dependencies under enableGlobalVirtualStore so linked children are materialized and slots remain isolated by their resolved link targets.
A headless install (--frozen-lockfile) now creates the command shims for a publicly hoisted workspace package's bin, matching what a normal install already did and what pnpm's own headless install does. Previously those shims were missing until the next non-frozen install.
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.
Fixed pnpm install ignoring a pnpm-lock.yaml that carries a leading env lockfile document when the file has CRLF line endings or a UTF-8 byte order mark, as a core.autocrlf checkout on Windows produces. The lockfile was reported as broken with multiple YAML documents detected and every dependency was re-resolved from the registry #13606.
When no directory above the project accepts a hard link — inside an AI agent sandbox that only grants write access to the project, or a container with just the project mounted writable — the default store is now created at <project>/node_modules/.pnpm-store instead of in the pnpm home directory. In those environments the home store is either read-only or on another volume, which forces every package to be copied instead of hard linked #13525.
A stray non-directory entry in node_modules no longer fails an install. Files placed next to the installed dependencies are skipped rather than reported as an unreadable manifest.
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.
Installing a workspace whose projects auto-install peer dependencies is substantially faster. Each round of the peer-hoist loop no longer scans the whole workspace once per project, so the cost of resolution grows with the workspace instead of with its square.
Installing a dependency chain whose packages carry peer dependencies no longer expands exponentially with the depth of the chain. A single project with a single such dependency could exhaust memory before finishing; it now resolves in tens of megabytes.
Fixed non-deterministic resolution on multi-project workspaces: two consecutive installs of the same inputs could bind peer-suffixed packages to different (still valid) providers, rewriting pnpm-lock.yaml on every install #13567.
Installing a workspace now produces the same pnpm-lock.yaml every time. Two installs of the same workspace could previously bind a peer dependency to a different — still valid — version, which changed the lockfile without anything in the project changing.
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.
Resolution on large peer-heavy workspaces got faster: a Bit workspace with 114 projects and ~21,000 lockfile entries resolves in ~13.4s instead of ~16.0s. The resolved dependency graph is unchanged.
Fixed nondeterministic peer bindings in large multi-project workspaces.
Resolving a workspace whose dependency chains are deep is faster: deciding which missing peer dependencies another project's resolution already covers now answers once per shared chain segment instead of once per report.
Peer resolution on large workspaces got faster: each hoist round now refreshes its view of the dependency graph from what the round changed instead of re-reading every resolved package. The resolved dependency graph is unchanged.
pnpm install no longer crashes on a machine whose system certificate store is empty or absent — for example a minimal container or build sandbox that ships no CA certificates #13588. Such a system now falls back to the Mozilla root certificates bundled into the binary, the same set Node.js ships, so both offline and online installs work again. Certificates from the system store, NODE_EXTRA_CA_CERTS, and the .npmrc ca / cafile settings keep taking precedence whenever any of them is available.
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.
pnpm login / pnpm adduser now read the scope setting from pnpm-workspace.yaml, the global config.yaml, and the PNPM_CONFIG_SCOPE environment variable, not only from the --scope command-line flag. When scope is configured, the granted token is keyed to that scope and the scope-to-registry mapping is recorded. --scope still takes precedence when both are set. Note that scope in an .npmrc is not read — pnpm keeps only auth and registry keys from that file.
Resolution spends less time in its final peer pass: the package-name cycle graph it consults is now derived once per package instead of once per occurrence of that package.
npm's --prefix is accepted as a spelling of --dir, and --store as a spelling of --store-dir, so pnpm --prefix ../ run test no longer fails with "unexpected argument '--prefix' found" #13583.
pnpm now ships node-gyp again, so packages whose install scripts shell out to it build out of the box. Previously they failed with spawn node-gyp ENOENT unless a node-gyp was already on PATH — affecting node-gyp-build with no matching prebuild, node-pre-gyp, a plain "install": "node-gyp rebuild", and any package shipping a binding.gyp without an install script. As in pnpm 11, the whole node-gyp dependency tree is resolved from pnpm's own lockfile when pnpm is released, so it is frozen per release rather than resolved on your machine, and npm_config_node_gyp, a workspace node-gyp, and a package's own node-gyp dependency all still take precedence.
With excludeLinksFromLockfile enabled, a link: dependency pointing inside the workspace is no longer treated as an external link when it resolves a pe
With excludeLinksFromLockfile enabled, a link: dependency pointing inside the workspace is no longer treated as an external link when it resolves a peer dependency, so the peer suffixes it produces stay identical to an install with the setting off. Injected (file:) workspace dependencies are no longer affected by the setting either #13556.
A registry dependency is now always recorded in pnpm-lock.yaml with an integrity hash, including under --lockfile-only. Packages from a registry that publishes no subresource-integrity metadata — https://node-registry.bit.cloud/, for one — were recorded without one, so the next pnpm install --frozen-lockfile failed with ERR_PNPM_MISSING_TARBALL_INTEGRITY #13547.
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.
Reduced memory use when resolving peer-heavy dependency graphs and prevented nested hoisted graphs from expanding into excessive dependency paths.
pnpm install no longer fails when pnpm-lock.yaml exists but cannot be parsed. Matching the TypeScript CLI, the install now prints an "Ignoring broken
pnpm install no longer fails when pnpm-lock.yaml exists but cannot be parsed. Matching the TypeScript CLI, the install now prints an "Ignoring broken lockfile" warning, resolves dependencies from the manifests, and rewrites the lockfile. --frozen-lockfile still fails on a broken lockfile.
Fresh installs no longer download the tarballs of platform-specific optional dependencies that don't match the current platform.
Your coding agent can read these notes before it upgrades. Set up the MCP server →