Recoverable React Compiler diagnostics are no longer logged by default. Set compiler.logDiagnostics to true to log them through Vite. Fatal diagnostics are always logged and fail the transform.
Respect environment sourcemap option for React Compiler transform when builder.sharedPlugins is enabled (#1439)
The React Compiler transform was using the top-level sourcemap option instead of the environment sourcemap option. This caused a problem when the experimental builder.sharedPlugins was enabled.
Fixed spurious window blur and focusout events and incorrect event.relatedTarget values when focusing an element after removing the previously focused element, which regressed in v30.1.0. (@asamuzaK)
Fixed focus and blur behavior across frames, and focusing the document's viewport through document.documentElement.focus(). (@asamuzaK)
Fixed focus targets removed or disabled by blur listeners becoming active, and text selections made by focus and blur listeners being overwritten. (@asamuzaK)
Fixed element.focus() incorrectly focusing disabled form controls and <input type="hidden"> elements with tabindex="". (@scttcper)
Fixed invalid style.setProperty() calls changing existing !important priorities, serialized styles, or mutation records. (@FedgeNo)
Fixed !important handling when updating CSS longhands after shorthands, using variables or CSS-wide keywords, and assigning style properties directly. (@FedgeNo)
Fixed <noscript> parsing with includeNodeLocations: true or inside frames to honor the runScripts option.
Fixed the storageQuota option being ignored by frames.
Fixed encoding detection of HTML and XML byte input to honor XML encoding declarations and detect UTF-16 without a byte order mark.
Fixed exceptions caused by truncated charset parameters in <meta> elements, and encoding detection incorrectly using incomplete <meta> tags. (@FedgeNo)
Fixed XML serialization errors for namespaces named constructor, toString, __proto__, or "null", and incorrect reuse of namespace prefixes declared on sibling elements.
Fixed element.innerHTML and element.outerHTML in XML documents to reject invalid characters in attribute values and avoid stack overflows on large strings.
Fixed selector matching for :lang(), :nth-child(... of ...) after mutations, and :has() with duplicate IDs or nested logical pseudo-classes. (@asamuzaK)
This release is dedicated to @scttcper, who unleashed @codex upon jsdom and found tons of performance improvements. Along the way, he found and fixed many correctness issues as well.
We really appreciate his thoughtful PRs, which did a great job following the project's contribution guidelines, and were clearly human-curated, with their PR descriptions edited to be brief and respectful of the maintainers' time.
Thanks to @scttcper, as well as all the other contributors of this release (most of whom were AI-assisted).
Added named access to elements on document, such as document.myForm for <form name="myForm">. (@vojtisprime11)
Added QuotaExceededError, including its use for storage quota errors and oversized crypto.getRandomValues() requests.
Added support for the relaxed DOM naming rules when creating elements, attributes, and document types.
Improved performance of DOM construction, tree mutations, range operations, and live collection access, especially on large documents. (@scttcper, @erezrokah)
Improved performance of getComputedStyle(), style changes, and CSS serialization. (@scttcper, @jhult)
Improved performance of event dispatch, form control and label lookups, and updates to <select> elements and radio button groups. (@scttcper)
Reduced memory use when creating and working with DOM nodes, attributes, event listeners, and mutation observers. (@scttcper)
Changed window.close() to preserve access to the document and its DOM through retained references.
Fixed element.querySelectorAll() returning no matches when the first part of the selector matches the element itself, which regressed in v30.0.0. (@asamuzaK)
Fixed case sensitivity in CSS attribute selectors, including selectors matching data-state="", title="", and other case-sensitive values. (@asamuzaK)
Fixed document.querySelector() failing to find a matching element when an earlier element has the same ID but does not match the rest of the selector. (@vojtisprime11)
Fixed :focus matching in shadow trees. (@asamuzaK)
Fixed DOM insertion and replacement, including valid document.replaceChildren() calls, invalid document element and doctype placements, and mutations during element.replaceWith().
Fixed the ordering of script execution, custom element callbacks, iframe loading, and mutation observer notifications during DOM insertion, including in shadow trees.
Fixed queued events and navigation continuing after window.close() or iframe removal, and prevented new scripts, resource loads, timers, and animation frames from starting in destroyed documents. (@scttcper)
Fixed parent documents waiting indefinitely for loading to finish when a child iframe removes itself during loading.
Fixed request cancellation across redirects, during pending requestInterceptor() callbacks, and when reusing an XMLHttpRequest after aborting it.
Fixed resource loading and JSDOM.fromURL() potentially hanging when response handling throws and response stream cleanup does not finish.
Fixed successful cached resource loads being treated as aborted.
Fixed getComputedStyle() and document.styleSheets using the wrong stylesheet order after inserting or updating <style> elements.
Fixed getComputedStyle() ignoring nested @import and @media rules in imported stylesheets, and returning stale results after imports finish loading.
Fixed style invalidation, stylesheet removal, and frame source updates in shadow trees.
Fixed repeated getComputedStyle() calls changing case-sensitive background URLs, and inconsistent resolution of border shorthands containing system colors. (@scttcper)
Fixed computed border widths, including borderless elements incorrectly reporting 16px, which regressed in v30.0.0. (@Alberto-BaseNet)
Fixed getComputedStyle() to resolve 'font-weight' keywords to numeric values. (@tianrking)
Fixed getComputedStyle() to convert lengths to pixels inside CSS math functions containing percentages, and to resolve percentages in 'font-size' math functions. (@soroushm)
Fixed serialization of min() and max() containing nested calc(), which regressed in v30.0.0. (@asamuzaK)
Fixed CSS values mixing lengths or percentages with math functions, such as 'grid-template-columns' values containing both 100px and calc(). (@rome-xi)
Fixed parsing of 'background' and 'border' shorthands with adjacent components, such as url(a.png)no-repeat, including a crash when parsing inline styles. Also fixed handling of invalid shorthand assignments and escaped or unusual characters in CSS declarations. (@asamuzaK)
Fixed parsing of unitless zero values in 'flex' shorthands, such as 35 1 0, and rejection of negative 'flex-basis' lengths and percentages. (@asamuzaK)
Fixed shorthand style assignments producing extra mutation records and custom element callbacks for intermediate values. (@scttcper)
Fixed Range and Selection handling of CDATA sections, including boundary offsets and range cloning, extraction, deletion, insertion, and stringification. (@scttcper)
Fixed text.normalize() incorrectly removing the text node or merging its siblings. (@scttcper)
Fixed cloning and importing CDATA sections and processing instructions whose contents have been modified, and serialization of CDATA sections adopted into HTML documents.
Fixed stale named-property collections on window, and incorrect named access from empty or namespaced id="" and name="" values. (@scttcper)
Fixed elements in documents created with DOMParser or document.implementation.createHTMLDocument() appearing as named properties on window and being retained in memory. (@Iaotle)
Fixed storage events being sent to windows created after the storage change, and ensured surviving recipients still receive events when the source document is destroyed.
Fixed attribute lookups after namespace prefix changes, and namespaced attributes incorrectly affecting ID lookups and element behavior. (@scttcper)
Fixed input.list in detached element trees. (@scttcper)
Fixed attr.ownerDocument after setting an attribute node on an element in another document or adopting its element. (@Kjubikstronk)
Fixed fresh element.getElementsByTagName() calls using the previous document's case-sensitivity rules after adoption between HTML and XML documents. (@Kjubikstronk)
Fixed element.tagName returning stale casing after adoption between HTML and XML documents.
Fixed radio button grouping and checkedness updates for controls with different form owners, controls outside their form, unnamed controls, and canceled clicks.
Fixed <select> selection updates when moving options, and unintended selection resets when moving a whole <select> or changing unrelated descendants.
Fixed cloning <input> elements to preserve input.indeterminate.
Fixed focus state after removing a focused element or its ancestor, including inside shadow trees.
Fixed script execution when inserting children into a connected, empty <script>. (@Kjubikstronk)
Fixed incorrect script execution after changes to src="", and handling of type="", for="", and event="".
Fixed document.currentScript during nested script execution and scripts in shadow trees.
Fixed event dispatch, window.event, and default passive listener handling for non-node event targets, and event dispatch when user code modifies window.constructor.
Fixed volumechange and ratechange events to fire asynchronously. (@christianaurichzm)
Fixed DOM APIs accepting user-created proxies around DOM objects where genuine DOM objects are required.
Fixed XPath iterator invalidation after DOM mutations and errors for invalid result types.
Fixed NodeIterator traversal when its filter removes nodes.
Fixed fileReader.readAsText() to honor the blob's MIME type charset when no supported explicit encoding is supplied.
Fixed fractional seconds in time input parsing and serialization. (@Jaybhade)
Fixed large input.valueAsNumber assignments for time and local date/time inputs.
Fixed hard wrapping of <textarea> values to use the numeric textarea.cols value.
Fixed document.readyState to be "complete" for documents created with document.implementation.createDocument() and document.implementation.createHTMLDocument().
Fixed element.translate for empty translate="" values and elements without a parent.
Fixed XML parsing of lone surrogates to replace them with the Unicode replacement character.
Fixed node.lookupNamespaceURI() for the xml and xmlns prefixes. (@vojtisprime11)
Fixed <base> elements to ignore data: and javascript: URLs. (@vojtisprime11)
Significantly improved and sped up getComputedStyle(). Computed value rules are now applied across a broader set of properties, and include fixes related to inheritance, defaulting keywords, custom properties, and color-related values such as currentcolor and system colors. (@asamuzaK)
Fixed CSS 'background' and 'border' shorthand parsing. (@asamuzaK)
Node.js v22.13.0+ is now the minimum supported v22 version (was v22.12.0+).
Other changes:
Overhauled the CSSOM implementation, replacing the @acemir/cssom and cssstyle dependencies with fresh internal implementations built on webidl2js wrappers and the css-tree parser. Serialization, parsing, and API behavior is improved in various ways, especially around edge cases.
Added CSSCounterStyleRule and CSSNamespaceRule to jsdom Windows.
Added cssMediaRule.matches and cssSupportsRule.matches getters.
Added proper media query parsing in MediaList, using css-tree instead of naive comma-splitting. Invalid queries become "not all" per spec.
pnpm 12.6.0 ships with automatic dependency deduplication, relocatable node_modules, package.yaml manifest editing, and --save-types support.
Minor Changes
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.
Patch Changes
Security
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.
Installing packages
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.
Resolving and linking dependencies
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.
Running scripts and tasks
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.
Workspace and project configuration
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.
Windows
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.
CLI commands and output
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 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 machine-wide concurrency limits. It also fixes an install that could reuse one package's downloaded tarball for another.
Minor Changes
Installing packages
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.
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.
Configuring pnpm
supportedArchitectures now accepts a list of platforms, in place of the os, cpu and libc axes.
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.
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.
Python interpreters and environments
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.
Python projects and workspaces
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.
Python dependencies and lockfiles
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.
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.
Patch Changes
Installing packages
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.
Cargo projects
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.
Python projects
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.
Performance
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.
Other commands
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, and Python lockfiles that work across compatible targets.
Patch Changes
Security
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.
Installing packages
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.
Resolving and updating dependencies
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.
Performance
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.
Python projects
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.
Workspaces and scripts
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.yamlpackages, 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.
CLI and output
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 faster.
Patch Changes
Installing packages
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.
Resolving and linking dependencies
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.
Performance
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.
Running scripts and tasks
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.
Commands
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.
Configuration
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.
Windows
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.
Messages and output
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.
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.
Add Python packages with 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.
Add Rust crates with 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.
Patch Changes
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.
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.
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 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 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 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.
Patch Changes
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 --versionpnpm/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.
Catalogs can now resolve workspace dependencies through the workspace: protocol.
Patch Changes
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 tokenHelperpnpm/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 publishpnpm/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.yamlpnpm/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 sideEffectsCachepnpm/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 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: 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:
^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:
A dependency cycle among the tasks of a run is now an error naming the participating tasks (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.
Under --no-bail, tasks whose dependencies failed are reported as skipped, not failed, and do not add to the exit code.
With --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.
Output is inherited rather than piped only when at most one script can ever be in flight (--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.
Patch Changes
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.
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 --versionpnpm/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.
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:
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.
pnpm install --frozen-lockfile false is no longer supported. Use pnpm install --no-frozen-lockfile to disable frozen-lockfile mode. To enable it, use pnpm install --frozen-lockfile without a separate true argument #14741.
Minor Changes
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:
A git-hosted dependency is prepared with the package manager it asks for. Its 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.
Patch Changes
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 11.28.1 makes pnpm install work in StackBlitz WebContainers, repairs lockfiles whose patch hashes disagree with patchedDependencies, and fixes many bugs in Windows command shims, injected workspace dependencies, and pnpm deploy.
Patch Changes
Installing packages
pnpm install now works in StackBlitz WebContainers. On projects without a lockfile, it used to fail with ENOENT ... pnpm-lock.yaml, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls. It also failed with "this.db.exec is not a function", because node:sqlite there lacks DatabaseSync.exec. When node:sqlite cannot prepare statements either, pnpm stores the index in index.fallback#15649.
pnpm install now completes after downloading a Node.js runtime specified by devEngines.runtime when pnpm runs on Node.js 24.4.x #14667.
pnpm install no longer fails when a package from the registry declares a file: dependency on a directory inside itself, such as "@types/css-tree": "file:./typings/css-tree". pnpm links that dependency to the directory inside the package, as npm and Yarn do. The lockfile records it as link:<root>/typings/css-tree#9141.
Installing a git-hosted dependency that has to be built no longer fails when that dependency's own dependencies have build scripts nobody approved. pnpm skips those builds while preparing the dependency, as it does without strictDepBuilds#9764.
pnpm now uses pnpm to prepare a git-hosted dependency that is a pnpm workspace without a committed lockfile. It used npm before, which could skip the dependency's build #14011.
When installing a git dependency over SSH fails with Permission denied (publickey), pnpm suggests checking the loaded keys with ssh-add -l. Resolving an SSH URL that refuses the key also shows a local HTTPS rewrite that leaves the recorded URL alone #13743.
pnpm install --dev and pnpm fetch --dev now install the optional dependencies of devDependencies, such as the platform binaries of Biome and oxlint. The project's own optionalDependencies are still skipped #9678.
pnpm install --frozen-lockfile now works on a detached HEAD when gitBranchLockfile is enabled. The install now reads the lockfiles of the local and remote-tracking branches that contain the checked-out commit. It still writes the shared pnpm-lock.yaml#7672.
pnpm install on CI now fails on an outdated lockfile when preferFrozenLockfile is explicitly set to true. Setting it to true used to let CI update the lockfile #9072.
pnpm install now fails with ERR_PNPM_IGNORED_BUILDS on a repeat install when strictDepBuilds is on and a dependency's build is still undecided. A repeat install against an existing node_modules reported success where a fresh install failed #10450.
pnpm install now removes an optional dependency from node_modules if its install script fails. Code that checks whether the package is installed no longer finds a package that cannot load #8756.
pnpm install --offline and pnpm add --offline now resolve a version range to the newest matching version whose tarball is already in the store. They used to pick the newest version in the cached metadata and fail with ERR_PNPM_NO_OFFLINE_TARBALL when its tarball was missing #10715.
If an offline install fails because the registry metadata cache uses the layout from before pnpm 11.27 and 12.4, the error now names the older mirror on disk and explains that one online install repopulates the cache #15656.
pnpm install now fails right away when writing package files fails because the store is full. It no longer retries the tarball download first #8581.
With nodeLinker: hoisted, pnpm install now restores a workspace project's node_modules after it was deleted. Before, the install printed "Already up to date" and left the project without the dependencies nested under it.
Under nodeLinker: hoisted, pnpm install now clears orphaned package directories that an interrupted or failed install leaves in a project's node_modules. A directory recorded by the previous install is removed, while an unrecorded directory is moved to node_modules/.ignored. A copy already in .ignored is never overwritten #13676.
Packages in an external virtualStoreDir can resolve the project's direct dependencies selected by hoistPattern. Run pnpm install --force to repair an existing installation #5652.
A repeat install now keeps the fast path when a declared local file dependency is replaced by an override #12892.
Store, build cache, and global virtual store
Files imported from the store now follow the umask of the install that writes them. Installing with a umask of 077 no longer leaves imported files readable by the group and others #3807.
With the global virtual store, pnpm rebuild no longer modifies packages shared with projects that have not approved their build scripts #12302.
The side-effects cache now restores the symlinks that a build script creates inside a package. A warm install used to replace each of them with a copy of its target #12859.
After upgrading, every package with a build script is built once more.
pnpm install now restores cached build artifacts when reinstalling a workspace that uses separate lockfiles #12942.
The global virtual store and the side-effects cache now key built packages by the Node.js version that the root project's devEngines.runtime or engines.runtime pins. That is the Node.js their build scripts run with. A dependency that declares its own engines.runtime no longer changes the key for every other package.
Concurrent installs that share a global virtual store now run a package's build in its shared slot one at a time. A failed build leaves the slot in place and marks it for the next install to rebuild #15568.
Concurrent installs no longer fail when they replace the same stale hoisted dependency link. Virtual store cleanup now keeps the temporary lockfiles that concurrent installs are still writing.
pnpm install keeps the owner, group, and mode of files already in a shared store, including index.db. New store files and directories inherit the store directory's group-write bit. When that directory is setgid, new files inherit its group. pnpm does not change a file's owner or group #12765.
When pnpm install repairs a store file that was modified through a hard link in node_modules, the repair now keeps the file's inode on Linux and macOS. Hard-linked copies in other projects are healed at the same time. Before, only the project running the install received the restored content. On Windows the repair still replaces the file, so other projects are healed on their next install #3445.
A tarball whose integrity pnpm computed during download is now found in the store on the next install. Before, that install downloaded the tarball again once the lockfile recorded the integrity #12562.
pnpm now warns when it cannot hard link packages from an existing store in the pnpm home directory and uses a store on the project's filesystem. This can happen when the project is on another filesystem, such as a bind-mounted workspace in a container. The warning names both stores and suggests setting storeDir#14505.
Resolving and linking dependencies
A peer dependency no longer resolves to two different versions for one package. This happened when the package peer-depends on another package and on one of that package's peers, and it is installed deeper than a direct dependency of the package that provides them #12098.
An optional peer dependency is no longer resolved from another workspace project's package when the project provides one of that package's own peers at a version it rejects. This avoids bogus unmet peer errors #13989.
pnpm no longer reports unmet peer dependency warnings for aliased npm: peer ranges that a tarball dependency satisfies #11126.
pnpm install now links the executables of auto-installed peer dependencies into the workspace root's node_modules/.bin, including after a frozen-lockfile reinstall #8511.
With resolutionMode: time-based and minimumReleaseAge both set, pnpm install no longer reports a subdependency as too new when only the time-based cutoff excludes it. Such subdependencies used to fail a strict install with ERR_PNPM_NO_MATURE_MATCHING_VERSION, or were added to minimumReleaseAgeExclude#13569.
A transitive dependency that has no matching version published before the time-based cutoff now resolves to the lowest matching version allowed by minimumReleaseAge. pnpm picks a version younger than minimumReleaseAge only if no older version matches #16298.
With minimumReleaseAge set, re-resolving the lockfile no longer rewrites the peerDependencies recorded for a package whose version did not change. This happened when the registry metadata of a package differed from the package.json in its tarball #13988.
pnpm no longer revalidates cached registry metadata when the registry sends Cache-Control: max-age=0, no-cache, or no-store. It downloads the metadata again, so a version newly published to such a registry is visible on the next install #13487.
pnpm's built-in package compatibility database no longer applies to a project's own manifest. A project named like a published package, such as vue-loader, no longer gains dependencies on pnpm install or pnpm update. User-configured packageExtensions still apply to project manifests #11700.
Relative local tarball paths in pnpm.overrides without an explicit file: prefix are now rebased correctly for workspace packages #11131.
pnpm no longer reports pnpm-lock.yaml as broken when a project depends on a package named constructor. A __proto__ key in the lockfile is now kept as a plain entry when pnpm reads or writes the lockfile. It no longer replaces the prototype of the objects pnpm builds from it #11028.
With nodeLinker: pnp, a workspace package can now require another workspace package it depends on #3567. On Windows, workspace dependency paths in the generated .pnp.cjs now use forward slashes.
Performance
pnpm now uses less memory when installing a package whose archive is larger than 64 MiB unpacked, and when installing a runtime from a zip archive, such as Node.js on Windows, Deno, or Bun #14164.
A fresh install reusing a warm global virtual store skips reimporting packages whose target directory is already complete #11112.
A warm pnpm install reuses on-disk package metadata for five minutes when the registry does not send an ETag. Registries that send an ETag, including the public npm registry, still revalidate with a conditional request. pnpm update still fetches current metadata #13976.
Patched dependencies
pnpm install now repairs a pnpm-lock.yaml whose (patch_hash=<hash>) dependency paths disagree with its patchedDependencies map, including paths that lack the hash their patch calls for. pnpm previously accepted such a lockfile as up to date and kept the old patched files. pnpm install --frozen-lockfile now fails on such a lockfile with ERR_PNPM_INCONSISTENT_PATCH_HASH. It fails with ERR_PNPM_UNCHECKABLE_PATCH_HASH when a patch hash in the lockfile is malformed, or when the lockfile lacks the package version or patch entry that the check needs #15336.
pnpm install now fails with ERR_PNPM_PATCH_NOT_FOUND when a patch file listed in patchedDependencies does not exist. It used to fail with a raw ENOENT error and a stack trace #5268.
pnpm now fails with ERR_PNPM_INVALID_PATCHED_DEPENDENCY when patchedDependencies has an invalid shape or contains a non-string value.
engineStrict now checks the patched package.json when a patchedDependencies entry changes engines. A patch that relaxes engines.node no longer fails the install against the published range #9603.
pnpm patch now applies the existing patch file to the edit directory of a git-hosted dependency, as it already does for packages from the registry #9699.
pnpm patch-commit now fails with an error when git cannot be found in PATH. It previously reported that no changes were found #8666.
Workspaces and filtering
pnpm install refreshes injected copies of workspace packages when source projects are rebuilt. Injected copies previously stayed stale until pnpm install --force#4407.
Scripts listed in syncInjectedDepsAfterScripts now update injected dependencies while they run. A watcher on the injected package, such as a dev server, sees each change before the script exits #4410.
pnpm install no longer fails for an injected workspace dependency whose package publishes from a publishConfig.directory that its own prepare script builds. The injected copy now picks up that directory once prepare finishes building it. pnpm install --frozen-lockfile no longer reports the dependency as outdated while the directory has not been built yet #7811.
With sharedWorkspaceLockfile: false, an injected workspace package that has lifecycle scripts is now hard linked into the projects that depend on it. Before, pnpm left a plain copy, so later edits to the package did not reach those projects #9828.
injectWorkspacePackages now hard links a workspace dependency declared with a relative path, such as workspace:../foo, the same way it already does for workspace:*#10446.
pnpm install no longer creates a node_modules symlink inside the publishConfig.directory of a workspace package linked with linkDirectory. A build tool that cleaned its output directory through that symlink deleted the files of the package's dependencies. pnpm install also removes a symlink that an earlier install left there #16226.
When verifyDepsBeforeRun triggers an install before a filtered pnpm run or pnpm exec, pnpm now installs only the selected projects and their dependencies. A later filtered command also installs a selected project that an earlier filtered install skipped #11865.
pnpm import in a workspace now keeps the versions pinned by a yarn.lock inside a workspace project #4385.
Installing with pnprServer set now records the pnpmfile checksum in the lockfile, so a later pnpm install --frozen-lockfile accepts that lockfile. A frozen install through the pnpr server now fails if the pnpmfile changed. If the pnpmfile defines a readPackage, afterAllResolved or preResolution hook or custom resolvers, pnpm resolves dependencies locally and prints a warning that the pnpr server was not used #14460.
Installing through a pnpr server now links a workspace project at the directory its publishConfig.directory names. An install that resolves through a server which does not forward the setting fails with ERR_PNPM_PNPR_PUBLISH_DIRECTORY_MISMATCH. The server rejects a publishConfig.directory that points outside its project.
Adding, updating, and removing dependencies
With autoInstallPeers, pnpm add and pnpm remove in a workspace project keep the locked version of a peer dependency the project declares. In a workspace where another project depended on a different version of that package, the peer could switch to that version #11225.
Adding a dependency now keeps unrelated transitive dependencies on their locked versions #11456.
pnpm update --recursive <pkg> no longer changes the version of a peer dependency that another workspace project installs automatically. Such a peer could move to a version outside the range the project declares, for example to React 19 in a project that declares react: ^18.3.1#14928.
pnpm update package@beta on a dependency declared as catalog: now updates the catalog entry and keeps catalog: in package.json. Before, pnpm wrote the resolved specifier to package.json#13399.
pnpm update now applies an override that references a catalog with the catalog's new value when the update bumps that catalog entry. Before, the packages the override targets kept the old version in the lockfile #12159.
pnpm add <dir> now warns when the added directory declares peer dependencies, as pnpm link does. The directory is saved as a link: dependency, and its peers are not resolved from the project that adds it. Use the file: protocol to have them resolved #5523.
Running scripts and commands
pnpm run and pnpm exec no longer install dependencies automatically when the root package.json still keeps overrides, packageExtensions, patchedDependencies, or ignoredOptionalDependencies in its pnpm field. pnpm no longer reads that field, so the install rewrote the lockfile without those settings. The command now fails and asks to move the settings to pnpm-workspace.yaml#16278.
pnpm run no longer reinstalls dependencies when a node_modules directory installed outside CI is used with CI=true, or the other way around #12337.
pnpm no longer hangs after a lifecycle script exits while a process it started in the background keeps the script's output open. pnpm stops reading that output one second after the script exits #5730.
A signal sent to pnpm, such as SIGTERM, now reaches the pnpm that pnpm switches to because of packageManager or devEngines.packageManager, and the one that pnpm with runs. The signal used to be dropped, so scripts running under that pnpm never got to shut down #9948.
pnpm run exits with the code of a script that handles Ctrl+C and shuts down. A script that finished cleanly is not reported as a lifecycle failure. The commands after it in the same script still run #9945.
pnpm run and lifecycle scripts use the configured scriptShell, including Git Bash on Windows, when shellEmulator is also enabled. shellEmulator still runs scripts when scriptShell is not set. Extra arguments passed to pnpm run are quoted for the shell that runs the script, so a Windows path stays intact #14719.
pnpm -r run /regexp/ now honors the tasksdependsOn declared for each script the selector matches, like running the script by name does. Matched scripts that depend on each other run in order. Each matched script runs once #15596.
Commands run from a POSIX shell through a dependency's own node_modules/.bin, such as node_modules/vite/node_modules/.bin/esbuild, no longer fail with MODULE_NOT_FOUND#10189.
pnpm rebuild with nodeLinker: hoisted no longer puts one package's parent node_modules/.bin directories on the PATH of the packages it builds after it.
Publishing, packing, and deploying
pnpm publish now waits at least 5 minutes for the registry to answer a publish request, like npm. This fixes "409 Conflict - Failed to save packument" errors when the registry is slow to answer #11454.
pnpm deploy with a shared lockfile now copies workspace dependencies into the deploy directory, even when packageImportMethod is set to hardlink. Previously, their files were hard-linked to the workspace sources, so editing a source file also changed the deployed copy #12176.
pnpm deploy --prod no longer fails with ERR_PNPM_OUTDATED_LOCKFILE when the deployed project declares a devEngines.runtime with onFail: download. The runtime stays out of the deployed node_modules with the rest of the dev dependencies #15703.
pnpm deploy --legacy no longer leaves broken links to nested local dependencies of workspace packages #9575.
pnpm deploy no longer creates extra directories inside the deploy target and workspace projects when using a relative deploy path #10981.
Manifests and configuration files
Settings given on the command line, such as --registry and --store-dir, now take precedence over the values a pnpmfile updateConfig hook sets #14063.
An updateConfig hook that returns registriesByScope without the default or @jsr entry no longer crashes the install with Invalid URL. A missing default keeps the configured registry, and a missing @jsr falls back to the built-in JSR registry #15619.
The hook's registry and the default entry of its registriesByScope now set one default registry, which installs, pnpm publish, and pnpm login all use. If a hook changes both, registry wins. A route that is not a string fails with ERR_PNPM_INVALID_UPDATE_CONFIG_RESULT.
An updateConfig hook in .pnpmfile.cjs has to use config.registriesByScope, config.registriesByPrefix, and config.registryOptionsByUrl. These registry lookups were renamed in pnpm 11.23.0 from config.registries, config.namedRegistries, and config.registryOptions#15620.
An async updateConfig hook that resolves to undefined now fails with ERR_PNPM_CONFIG_IS_UNDEFINED, as a synchronous hook that returns undefined already did.
pnpm config set --location=project and pnpm config delete --location=project, run from a package inside a workspace, now write settings that belong in pnpm-workspace.yaml to the workspace root's pnpm-workspace.yaml. Before, they created a new pnpm-workspace.yaml in the current package, which made that package the workspace root. Settings stored in .npmrc are still written to the current directory #13757.
pnpm now reads the workspace directory override from PNPM_CONFIG_WORKSPACE_DIR, like other settings. NPM_CONFIG_WORKSPACE_DIR still works as a fallback #16275.
A ${VAR} placeholder in .npmrc or pnpm-workspace.yaml whose name matches a built-in object property, such as ${toString}, is now treated as an unset variable. It used to be replaced with the source text of a JavaScript function.
pnpm now rejects a falsy non-array packages field in pnpm-workspace.yaml, such as packages: false, with an error. It used to treat the field as omitted.
Global packages, pnpm versions, and runtimes
pnpm update --global now reinstalls the global packages that pnpm 10 installed into the previous global directory, <global-dir>/5, so their commands are linked into the pnpm home bin directory again and pnpm list --global lists them. Once every package is migrated, pnpm deletes the previous directory and the commands pnpm 10 linked into the pnpm home #11528.
pnpm env remove --global deletes Node.js versions that pnpm installed into its own store, including when another tool installed pnpm #8357.
pnpm self-update no longer suggests a downgrade when minimumReleaseAge holds back the registry's latest release. It now says that release is still within the cutoff #12006.
@pnpm/exe no longer ships a binary for arm64 musl Linux, such as Alpine on ARM. The published binary crashed with a segmentation fault at startup. Installing @pnpm/exe on that platform now fails with an error that suggests npm install -g pnpm or pnpm 12 #10443.
The macOS and Linux release archives no longer include Windows-only files, such as node-gyp.cmd and the fastlist executables #11352.
Windows and WSL
On Windows, pnpm run now passes the arguments after the script name to the script as typed. Before, cmd expanded %VAR% in them and backslashes arrived doubled. Line breaks still arrive as the two characters \n, because cmd cannot pass them. The command line pnpm prints for the script quotes the arguments the same way on every platform #16257.
On Windows, command shims now run tools whose paths contain non-ASCII characters #6999. The PowerShell shims do so in Windows PowerShell 5.1 too #16217.
On Windows, the .cmd command shims in node_modules/.bin now keep a % in the project path. Before, cmd.exe expanded it as a variable reference, so the command received a mangled NODE_PATH#15716.
Bin shims in node_modules/.bin run from Cygwin on Windows again. The shims passed a /cygdrive/c/... path to the Windows node found on PATH, so Node.js failed with Cannot find module 'C:\cygdrive\c\...'#12845.
pnpm setup no longer writes the pn.ps1, pnpx.ps1, and pnx.ps1 PowerShell wrappers. It also removes the ones an earlier setup wrote. PowerShell now runs pn, pnpx, and pnx through their .cmd wrappers, like pnpm itself. Before, these aliases failed with a "not digitally signed" error wherever the execution policy blocks unsigned scripts #8444.
On Windows, globally installed @pnpm/exe commands now run in the invoking PowerShell console and return their exit status #6503.
On Windows, installing @pnpm/exe with npm inside a project now writes node_modules/.bin shims that run the standalone executable #15688.
On Windows, pnpm env use -g and pnpm add -g node@runtime:<version> now replace a node.exe in the global bin directory that is a broken symlink. Previously they failed with ENOENT#5411.
On Windows, pnpm expands nested %VAR% references in PNPM_HOME and the other directory environment variables it uses for its home, store, cache, state, and config directories. pnpm fails with an error when a %VAR% reference remains after expansion #13236.
On Windows, if the global bin directory is not in PATH and a PATH entry still contains an unexpanded variable such as %PNPM_HOME%, the error now names that entry. A variable referenced from the user Path must be set to a full path and stored as a plain string (REG_SZ) for the entry to expand #5283.
On Windows, pnpm setup no longer garbles non-ASCII characters in existing Path entries #6346.
On Windows, pnpm setup repairs the PNPM_HOME registry type left by older pnpm versions, even when the configured directory has not changed.
On Windows, the ERR_PNPM_BAD_ENV_FOUND error of pnpm setup now shows the value PNPM_HOME is currently set to. It used to show the directory pnpm wanted to set.
On Windows, pnpm install no longer skips a dependency's build script on a later install when the script changes nothing inside the package directory #15667.
On Windows, pnpm now retries writing the workspace state file while another process, such as an antivirus scanner, briefly holds it open #14550.
Inspecting dependencies
pnpm licenses list now reports the actual on-disk package locations when using nodeLinker: hoisted or shamefully-hoist: true#8589. pnpm licenses list --json now includes every installed copy of a package in its paths array, including hoisted copies and isolated installations with different peer dependencies. Its paths also exist on disk when the isolated linker uses a custom modulesDir.
pnpm list now shows the correct path of a link: dependency that points to a directory on another drive on Windows. The path used to be appended to the project directory, such as C:\project\D:\lib, and pnpm list --long could not show the package's details #10362.
pnpm root now prints the configured modulesDir. It used to print node_modules regardless of the setting. A project's own modulesDir from packageConfigs is printed too #9113.
pnpm audit and pnpm audit signatures now fail with an error when the lockfile contains unresolvable dependency references #13638.
Output and messages
With the default and append-only reporters, installs with --loglevel warn or --loglevel error now print the full output of a failed install script. The output of successful scripts, including the root project's own install hooks, stays hidden. With --loglevel warn, pnpm also prints ignored build script warnings.
The lockfile verification error now suggests relaxing the policy that flagged an entry only if a fresh resolution still fails and you trust the affected packages. Errors from checks that no policy controls, such as a missing tarball integrity, no longer suggest relaxing a policy #14411.
pnpm run --recursive now prints GitLab CI collapsible sections that GitLab recognizes. The section markers used to appear as raw text in the job log.
Local tarball dependencies using the file protocol are no longer counted as downloaded in the progress banner #1103.
pnpm 11.28.0 adds the forceIgnoresPlatform setting and pnpm update --peer, and fixes many bugs in pnpm deploy, --filter, nodeLinker: hoisted, and custom modulesDir setups. This release also carries security fixes for shell completion, bin shims on Nix, lifecycle scripts inside a custom modulesDir, and userAgent placeholders in pnpm-workspace.yaml.
Minor Changes
Added the forceIgnoresPlatform setting. When it is false, pnpm install --force skips optional dependencies whose os, cpu or libc do not match the host instead of installing all of them. The default stays true#6133.
Patch Changes
Security
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.
pnpm no longer treats packages inside a custom modulesDir as workspace projects, including one that packageConfigs sets for a project. Before, with a modulesDir such as vendor and a packages pattern such as **, a repeat install ran the lifecycle scripts of dependencies that allowBuilds had not approved #15412.
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.
Shell completion now omits candidates containing control or invisible formatting characters, and fish completion omits names containing backslashes. Package and script names can no longer inject extra completion records or terminal escape sequences.
Commands that run pnpm again, such as pnpm runtime set and pnpm env use, no longer re-run a script that only looks like pnpm. A script named pnpm or pn that another package installed was run as though it were pnpm.
pnpm store prune now leaves a dlx cache root that is a symlink or Windows junction untouched. Cleanup no longer removes directories through that link.
Installing packages
pnpm install now fails at once when a registry or tarball server presents a TLS certificate that fails verification, such as a self-signed or expired one. The error names the certificate problem. Such requests were retried for more than a minute #9134.
pnpm install no longer appears to hang when a git dependency is fetched over SSH and ssh asks for a key passphrase or a host key confirmation. pnpm now runs ssh in batch mode, so the install fails right away with the ssh error, and a key that needs a passphrase has to be loaded into an SSH agent first. An ssh command selected through GIT_SSH_COMMAND, GIT_SSH, or the core.sshCommand git setting is kept as is #2227.
pnpm no longer crashes on startup when the temporary directory set by TMPDIR, TEMP, or TMP does not exist #4960.
pnpm install --silent no longer fails when the install is delegated to pacquet. pnpm also stops passing -s, --loglevel and the other reporting flags to pacquet #11936.
Fixed pnpm install failing with EEXIST when a concurrent install cleared the file or directory that was occupying a symlink path. On Windows, a symlink another process is still holding is no longer moved aside and recreated.
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.
Installing or adding dependencies no longer fails when a previously installed local tarball file was deleted from disk #8367.
pnpm install now reads the same local tarball it installs when a dependency's absolute file: path contains ... Such a path could install a different tarball than the one it read, failing with ERR_PNPM_TARBALL_INTEGRITY, or fail to resolve at all.
pnpm install --frozen-lockfile now rejects changed local tarballs, even when the previous archive contents are in the store #1889.
pnpm add and pnpm install now support installing bzip2 compressed tarballs #6761.
pnpm install now fetches committed submodules of git dependencies #1470.
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 and pnpm run now reinstall a single project that was moved or renamed together with its node_modules. Before, they reported "Already up to date" while links such as Windows junctions still pointed at the old location #9512.
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 and pnpm add no longer skip optional dependencies that the Node.js version resolved for a devEngines.runtime range supports, when the range uses onFail: download. An explicitly set nodeVersion still takes priority #14628.
pnpm install now uses the running Node.js when devEngines.runtime declares a range without onFail: download. Optional dependencies supported by the active Node.js are no longer skipped #15230.
pnpm install --engine-strict now respects engines relaxed by readPackage hooks in .pnpmfile.cjs#15482.
pnpm install now applies changes to or removal of a global readPackage hook when an existing lockfile is present #15136.
The project's .pnpmfile.mjs or .pnpmfile.cjs now runs after the pnpmfiles of config dependency plugins #9891.
A readPackage hook that sets a dependency range to a value other than a string, such as undefined, now fails the install with an error that names the dependency, the package, and the pnpmfile. Delete the property to remove a dependency #5517.
pnpm install --prod and other installs that skip devDependencies no longer run the pnpm:devPreinstall script #7065. They skip prepare lifecycle scripts too, as does pnpm install given package arguments. pnpm deploy does not run the prepare scripts of the deployed project #7282.
The root project's preinstall script now runs before dependencies are resolved and linked. A guard such as npx only-allow yarn can stop the install before pnpm populates node_modules#3760.
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 prune --prod and production installs now prune excluded development dependencies even when lockfile generation is disabled.
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.
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.
Resolving and linking dependencies
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 and pnpm update now resolve a dependency range to the newest matching version that is not deprecated. A version already recorded in the lockfile is still used #15128.
pnpm add and pnpm remove no longer move unrelated transitive dependencies to other versions. Adding a package and then removing it now leaves pnpm-lock.yaml unchanged. Before, the dependencies of auto-installed peers and npm: aliased subdependencies could move to a newer version that was already in the lockfile #11859.
pnpm dedupe now moves transitive dependencies to the version a catalog: dependency pins, as it already did for versions written directly in package.json.
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 --frozen-lockfile now succeeds when an optional dependency was unresolvable and skipped by the install that wrote the lockfile. Previously, frozen installs failed with ERR_PNPM_OUTDATED_LOCKFILE. The notice states that the dependency could not be resolved and names the requested range #3960.
pnpm no longer rewrites packageManagerDependencies in pnpm-lock.yaml when that block pins @pnpm/exe beside pnpm. The rewrite ran on every command, so pnpm list left a clean working tree dirty, and pnpm version then refused to run #14926.
pnpm import and fresh resolutions now record integrity for git-hosted tarballs, such as codeload.github.com URLs, even when the tarball is already in the store #13338.
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.
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.
Workspace projects that hoistPattern or publicHoistPattern selects are now hoisted on every install. A project added to the workspace was not hoisted until node_modules was deleted and reinstalled. A workspace that installs nothing from a registry hoisted none of its projects at all #3642.
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 now links a dependency's bin even when the bin's file does not exist yet, such as a workspace package's bin that a build script creates after install. Previously pnpm printed a Failed to create bin warning. The command then stayed missing until node_modules was removed #10007, #10216.
Executable linking now makes a bin executable for every user. A target with only some execute permission bits set was left non-executable for other users #3699. Bin linking leaves workspace and linked dependency files outside node_modules unchanged.
Dependencies and executable binaries are now correctly linked and accessible for workspace packages using publishConfig.directory and publishConfig.linkDirectory#8338.
Workspaces and filtering
Fixed pnpm install for workspace projects reached through a symlink, such as a packages directory that links to a folder outside the workspace. pnpm now 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. A workspace project with build metadata is no longer skipped when it matches the requested range and has the same version precedence as the registry package #2812.
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 --frozen-lockfile now fails when a workspace package's version no longer satisfies the range that a dependent workspace project declares for it. This includes injected workspace dependencies #7823.
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.
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.
Fixed injected workspace dependency synchronization failing with EPERM on Windows when removing nested directories.
Workspace discovery now returns one project per directory when multiple manifest formats are present. It selects package.json, then package.json5, then package.yaml#3027.
Wildcards in negated packages patterns of pnpm-workspace.yaml now match directories whose names start with a dot. For example, !packages/** now also excludes packages/.dev/tool when another pattern includes .dev explicitly.
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.
pnpm now warns when shared-workspace-lockfile is passed on the command line outside a workspace #1617.
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.
Directory filters such as --filter=./packages/* now select projects when the current directory was entered with a lowercase drive letter on Windows, like c:\repo#5500.
Custom modulesDir
pnpm run, pnpm exec, pnpm version hooks, and the lifecycle scripts a project runs during install now find commands in a custom modulesDir, and bin shims resolve dependency paths there #3604. Project .hooks scripts are read from the configured modules directory. Installs resolved by pnpr now preserve configured modules and executable directories. pnpm bin now reports the configured executable directory. In a workspace whose projects keep their own lockfiles, a packageConfigs entry that gives one project its own modulesDir is followed too.
Tools installed in a custom modulesDir can load CommonJS plugins installed there, the same way they would from node_modules. When executables are symlinks, as with preferSymlinkedExecutables or the hoisted linker, this works for pnpm run, pnpm exec, pnpm version hooks, and the lifecycle scripts a project runs during install. A symlinked tool started directly from a shell does not get it. Paths containing the platform path-list separator do not receive this fallback. extendNodePath: false disables this fallback #3604.
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.
Adding, updating, and removing dependencies
pnpm add now keeps the specifier a readPackage hook provides when the hook rewrites the requested one. When the hook removes the dependency instead, the add skips it and reports why. Previously the add wrote the request either way, and the hook undid it on the next read, so pnpm install --frozen-lockfile failed #15156.
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 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.
Added a --peer flag to pnpm update to update ranges in peerDependencies#8081.
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 <name> now updates a peer dependency that pnpm installed automatically #10486.
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 runs the project's own preuninstall, uninstall, and postuninstall scripts. preuninstall and uninstall run before dependencies are unlinked. A failure in either stage aborts the removal. postuninstall runs after unlinking completes. The ignoreScripts setting and --lockfile-only skip all three stages #3276.
pnpm remove -r now fails if any requested dependency is absent from all selected workspace projects. Validation respects --save-prod, --save-dev, and --save-optional and completes before modifying project manifests #2319.
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.
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.
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-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.
make-dedicated-lockfile no longer removes fields such as main and types from the publishConfig of the project's package.json. It now restores package.json when it cannot move the original node_modules back to its place. The error then names .tmp_node_modules, where the original node_modules was left. The command refuses to run while that directory exists, so a retry cannot overwrite it.
Running scripts and commands
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 for child processes #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 --recursive no longer reports interrupted scripts as lifecycle failures after Ctrl+C.
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 install-test now accepts --no-bail when executed directly and in recursive runs #3777.
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.
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.
If a script is killed by a signal that pnpm survives, such as SIGPIPE, the error now names the signal: Command failed with signal SIGPIPE.#9821.
Publishing, packing, and deploying
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 honors the files field of package.yaml and package.json5 manifests. Git-hosted and injected local dependencies that use these manifests now honor it too #7906. The archive includes exactly one package.json when the project uses an alternative manifest format, even when .npmignore or files excludes the source file.
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 no longer triggers an install when running scripts in a read-only deployed filesystem #11617.
A legacy pnpm deploy with node-linker=hoisted now puts the deployed project's direct dependencies at the top of the deployed node_modules#9671.
pnpm deploy --legacy no longer rewrites node_modules/.pnpm-workspace-state-v1.json in the source workspace. The next verifyDepsBeforeRun check there reported the workspace as out of date #15352.
Manifests and configuration files
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 now preserves CRLF line endings when it modifies project manifests.
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 change check now validates the pending change intents in .changeset/. It fails when an intent names a package that is not in the workspace or cannot be released.
.npmrc 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#12828.
pnpm now warns when an empty environment variable removes an .npmrc authentication token. Authentication environment warnings now name the affected key #4806.
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 treats a missing global config.yaml, auth.ini, or other optional config file as absent in Node.js-compatible runtimes such as StackBlitz WebContainers. Commands such as pnpm --version failed there with ENOENT#14030.
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.
pnpm doctor now checks the configured default registry and sends its credentials. It used to always ping https://registry.npmjs.org/#15618.
Global packages, pnpm versions, and runtimes
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 now recover a global package group whose entire node_modules directory was deleted. pnpm remove -g leaves such a group's command shims in the global bin directory #15093. These commands 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 update -g no longer asks more than once for approval of the same immature name@version when minimumReleaseAgeStrict is enabled #15091.
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 no longer deletes aliases and other lines that sit between a # pnpm comment and the pnpm block in a shell startup file #7067.
pnpm env remove now cleans up dangling Node.js executables and symlinks. 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.
Windows and WSL
pn, pnpx, pnx, and pnpm now run when Git Bash, MSYS2, or Cygwin launches them through a Windows path such as C:\Users\me\node_modules\pnpm\pn. The aliases used to fail to find the pnpm installed beside them, or hand the call to an unrelated one #14884.
Fallback .cmd and .ps1 Windows wrappers in @pnpm/exe now propagate the exit status of the invoked pnpm command #14826.
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.
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.
On Windows, pnpm now fails within about a second when it cannot move a node_modules directory installed by another package manager because a file in it is in use. The error names the directory and suggests stopping the process that uses it. pnpm used to retry for a minute and then print a raw EPERM stack trace #7505.
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.
pnpm install now resolves local tarballs specified with bare UNC paths on Windows #1669.
pnpm now recognizes local paths with forward slashes on Windows.
On Windows, pnpm add and pnpm update now write relative file: and link: specifiers with forward slashes to package.json and the lockfile. They used to write backslashes, so the same project produced different files on Windows and on other systems #7497, #9687.
Fixed scripts failing with errors such as 'an-compile' is not recognized when scriptShell is set to cmd.exe on Windows #7181.
Inspecting dependencies
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=update now fixes vulnerabilities in dependencies declared through an npm alias. A specifier such as "foo": "npm:vulnerable-pkg@1.0.0" moves to the patched version and keeps the alias. Versions pinned with a leading = are fixed as well #15155.
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 paths under node_modules/.pnpm that do not exist. It now reports the directory where the hoisted linker placed each package #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 no longer replaces the project's own license or bugs field with the workspace root's value when the project's value is blank. The same applies to an author, description, license, repository, or bugs field set to null#14882.
pnpm store status no longer reports packages with build or postinstall scripts as modified in the store. 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 pnpm:peer-dependency-issues log event, which --reporter ndjson prints, no longer lists peers silenced by peerDependencyRules.ignoreMissing under conflicts or intersections#8295.
Output and messages
The error for an incompatible pnpm-lock.yaml now reports the lockfileVersion the file was generated with and the lockfileVersion the current pnpm supports. The error also warns that recreating the lockfile with --force may break the application and suggests installing the pnpm version that generated the lockfile #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.
Resolution no longer logs an error when a package metadata request fails and resolution succeeds via cached metadata #2522.
The ignored build scripts warning and the update notice are printed as plain lines when output is not a terminal, in CI, or with --reporter append-only. They were drawn inside a box that broke apart in CI logs #9421.
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 add now warns when replacing an existing dependency with a specifier pointing to a different source #14869.
pnpm update no longer warns "Skip adding ... to the default catalog" for a dependency that already uses catalog:#13715.
pnpm remove --help no longer shows a [@<version>] suffix in its usage line. The command accepts package names only #7751.
Bash completion now completes script names that contain a colon, such as pnpm run test:u to pnpm run test:unit#5482.
pn, pnpx, and pnx now run the pnpm installed alongside them. They used to look pnpm up on PATH. That failed when the directory holding them was not on PATH, and it silently handed the call to an unrelated pnpm when one came first there #14803.
The @zkochan/cmd-shim package is now available as @pnpm/bins.cmd-shim.
pnpm cache list-registries now prints the registry URL, matching pnpm cache view. It printed https%3A+registry.npmjs.org before and prints https://registry.npmjs.org/ now #15046.
pnpm deploy no longer installs the dependencies of the workspace root project into the deploy directory #6437.
Install warnings no longer carry the text of a package's deprecation notice. The warning names the deprecated package and version, and the pnpm:deprecation event no longer carries the notice either. pnpm view still shows it on request.
A deprecation warning now names the newest version of the package that is not deprecated, and says when reaching it means widening the range you declared:
WARN deprecated foo@1.0.0. 2.3.1 is not deprecated, outside the range you declared.
pnpm works this out from the metadata it already fetched, so it costs no extra request. An install that reuses the lockfile without fetching metadata names no version.
pnpm strips control characters from the package name and version in a deprecation warning, and from the notice pnpm outdated --long prints.
The text sanitizer now also strips the Unicode line and paragraph separators U+2028 and U+2029.
pnpm exec <command> and pnpm <command> run from a subdirectory of a project now find the executables installed in the project's node_modules/.bin. The command still runs in the subdirectory. PNPM_PACKAGE_NAME names the project #5068.
pnpm exec and pnpm dlx now wait for the command to finish shutting down after Ctrl+C. A signal sent to pnpm alone now reaches the command, the way it does with pnpm run. pnpm used to exit on the interrupt and terminate the command while it was still shutting down #7374.
Warnings about ignored environment variables in project .npmrc credentials now link to the npmrc documentation pnpm/pnpm#15051.
pnpm audit --interactive --fix=update no longer opens a second prompt for selecting dependencies to update #14927.
Fixed pnpm dedupe requiring a second pass after bumping a direct dependency in package.jsonpnpm/pnpm#14987.
pnpm deploy now writes plain versions for registry dependencies with peer dependencies in the deployed package.json. The deployed lockfile retains the resolved peer bindings. npm aliases keep their target package names #14873.
pnpm publish now allows a detached Git HEAD in CI, including checkouts of release tags. The working tree must still be clean. Branch and remote-history checks still apply when HEAD is attached pnpm/pnpm#5894.
pnpm dlx and pnx now prompt to approve dependency build scripts in interactive terminals. Cached packages with pending builds also prompt for approval. Without an interactive terminal, use --allow-build to allow the required builds. Fixes pnpm/pnpm#14943.
pnpm install --force now removes obsolete dependency links inside virtual-store packages when their dependencies change. Invalid dependency names are ignored during obsolete-link cleanup #15039.
pnpm add -g and pnpm update -g now ignore incomplete unrelated global package groups when every command from the replaced group is retained. Operations that could remove a global command still require complete ownership information.
Resolving a Node.js runtime now fails when unofficial-builds.nodejs.org cannot be reached. pnpm used to ignore that failure and leave the musl builds out of pnpm-lock.yaml. pnpm update then wrote a different lockfile on a machine whose network blocks the mirror pnpm/pnpm#14813.
pnpm now deduplicates a package whose child dependency resolved an optional peer in one workspace project but not in another. Two copies of next could appear when only some projects could reach styled-jsx's optional babel-plugin-macros peer #14800.
Fixed shell completion of package scripts for pnpm run and pnpm run-scriptpnpm/pnpm#15034.
Bash completion now preserves literal script names containing glob characters and shell punctuation in pnpm v11 and v12.
pnpm sbom now publishes a valid URL in the CycloneDX externalReferences[].url and the SPDX homepage. An npm shorthand such as vercel/ms or gitlab:group/subgroup/project is expanded to the git+https URL npm derives for it. An scp-style remote such as git@github.com:vercel/ms.git is expanded the same way. Any other URL is published in its normalized form, without embedded credentials. A value that names no repository, an email address for example, is left out. pnpm used to publish the raw value, so a shorthand produced a URL that consumers such as Dependency-Track reject pnpm/pnpm#14773.
pnpm setup now describes the displayed configuration changes as "the following configuration changes."
pnpm --version now reports why the pnpm version a project pins cannot be installed or recorded, then prints the version of the running CLI. It used to fail, which made the command unusable where the filesystem is read-only. pnpm --version also honors --store-dir and its --store alias now #14831.
pnpm install --force now reinstalls dependencies when the manifest and lockfile are unchanged. It previously reported "Already up to date" without reinstalling. Files changed in node_modules are restored when the store content is intact. Combining --force with --frozen-store now reports a configuration conflict on repeat installs #919.
pnpm install now installs git-hosted dependencies without preparing them when their builds are explicitly denied by allowBuilds. Dependencies that require preparation still need an explicit allow or deny decision pnpm/pnpm#10522.
pnpm runtime set and pnpm env use now use the pnpm version that started the command. They could run a different installed pnpm when the command was started through Corepack or another wrapper.
The install summary now names the version each dependency resolved to when node-linker is hoisted. It also lists what an install restores after node_modules is deleted, and both sides of a version change. The summary showed the range recorded in package.json, or nothing at all #15161.
The @pnpm/npm-lifecycle package is now available as @pnpm/exec.npm-lifecycle.
Fixed minimumReleaseAge making pnpm download a package's full metadata again on every install. The cached copy carried a validator the registry could not match, so pnpm could never revalidate it pnpm/pnpm#15103.
pnpm now measures a pnpm.overrides entry written as a bare path, such as ./local-dep, from the directory holding pnpm-workspace.yaml. It used to be measured from each package the override rewrote, so the dependency linked to a directory that does not exist #11131.
pnpm now preserves scalar YAML anchors and aliases when editing pnpm-workspace.yaml. Removing the entry that defines an anchor keeps surviving aliases valid. Entries updated to different values are written separately #8245.
pnpm now preserves comments and existing key order when updating package.yaml. New keys are appended to their mapping pnpm/pnpm#2008.
pnpm install --prod no longer downloads the registry packages that only a devDependency reaches #881.
pnpm update --global no longer reinstalls a global package when its dependency graph resolves to what is already installed. It reports Already up to datepnpm/pnpm#12002.
The minimumReleaseAge approval prompt now counts and displays each package version once pnpm/pnpm#15083.
pnpm run no longer sends a script a second SIGINT when Ctrl+C is pressed in a terminal. A script that shuts down on the first SIGINT and exits at once on a second used to die before its shutdown finished #7374.
pnpm now reads a pnpm-workspace.yaml whose tasks section uses a setting only pnpm 12 acts on, such as concurrencyGroup. A task's unrecognized fields are ignored, unless the field only differs in case from concurrency or dependsOn, which pnpm reports as a typo.
The warning about unrecognized top-level settings now names cargo, concurrencyGroups, and pipelines as pnpm 12 settings.
pnpm -r list --json now prints one JSON array. It printed a separate array for each project when sharedWorkspaceLockfile was false, so the output could not be parsed.
pnpm -r list now reads each project's own modules directory when the projects keep their own lockfiles, so --long and --parseable report the packages that project installed #15011.
A signal sent to pnpm while it runs without a terminal, as a container runtime or a service manager does, now reaches the script even when the shell running it stays the script's parent. pnpm then waits for the script to finish shutting down. Such a signal used to end the shell at once or stay with it, and the script was never told to stop #7374.
Fixed minimumReleaseAge being skipped for packages served by a registry that returns the same ETag for abbreviated and full package metadata pnpm/pnpm#14925.
pnpm install now returns "Already up to date" in a workspace where dedupeDirectDeps left a project without a node_modules directory of its own. Such a project forced a full install on every run.
Installs in different projects that share a global virtual store no longer fail on Windows with Access is denied while repairing the same slot #15114.
pnpm sbom now emits a license value as a CycloneDX expression only when it is a valid SPDX license expression. Anything else is emitted as a CycloneDX license name pnpm/pnpm#14786.
A dependency's own bins can no longer take over another package's bin shim. The POSIX shims pnpm generates used to look up their shell helpers on PATH, where a dependency's bins come first #14837. Reinstalling replaces the shims already in your node_modules. On Cygwin, MSYS2, and WSL the shims still take their Windows path conversion from PATH, so a dependency can still redirect them there.
POSIX bin shims now convert a Windows-form path such as C:\node_modules\.bin\tsc correctly. The shim mangled the backslashes in such a path and could not reach the package it runs. Installing again replaces the shims already in node_modules#14867.
pnpm pack now writes tarball entries grouped by file extension and file name, the order npm uses. Packages that ship many same-named files, such as template collections, pack much smaller #14766.
A command run in a project that the workspace does not include now acts on that project alone. A project is outside the workspace when it has a manifest of its own and no pattern in the packages setting selects it, or when a ! pattern excludes it. A directory with no manifest of its own, such as a package's source directory, still belongs to the workspace. pnpm install in an excluded project used to install every project in the workspace #3561.
POSIX bin shims now take cygpath and wslpath from the system default path on Cygwin, MSYS2, and WSL2. The shims looked both helpers up on PATH, where a dependency's own bins come first, so a dependency could redirect another package's shim. Installing again replaces the shims already in node_modules#14866.
pnpm update and pnpm audit --fix=update no longer copy dependencies added by packageExtensions, a readPackage hook, or an override into package.json. Those dependencies keep the specifier the hook or override gives them. pnpm update --latest no longer resolves past that specifier. pnpm audit --fix=update now warns when one of them pins a vulnerable version. The warning points at pnpm audit --fix#14928.
nodeDownloadMirrors can now be set in the global config file (config.yaml) and through the PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS environment variable, so a Node.js download mirror can be configured once for a machine instead of in every workspace #12124, #13611.
Added a new setting trustPolicyExcludePrune (default: false). When enabled, pnpm add, pnpm update, and pnpm remove prune the entries of trustPolicyExclude 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.
Patch Changes
pnpm now reads the packageManager, devEngines.packageManager and runtime pins from the workspace root's package.json when lockfileDir is set. A project that moved its lockfile lost the pins it declared there #14633.
Fixed pnpm add -g, pnpm update -g, and pnpm remove -g mutating global bins or install directories after only partially reading an installed package group. If any declared package manifest is missing, malformed, or unreadable, pnpm now fails before activation or removal and leaves the existing global installation intact pnpm/pnpm#13796.
fetch-timeout now limits how long a request may make no progress. The timer restarts on every chunk that arrives. A large download over a slow connection is no longer aborted while data is still coming in. A connection that stops delivering data still fails after fetch-timeout#14604.
pnpm peers check no longer reports a peer dependency declared as workspace:^, workspace:~, or a bare workspace: as unmet. pnpm reported these as unmet whatever version the linked workspace project supplied #14770.
A readPackage hook that edits its argument in place no longer changes what a later install in the same command resolves. A deprecated notice read from the lockfile no longer carries over to another install either #13988.
pnpm install now auto-installs missing transitive peers when workspace projects share a dependency at different depths. This also removes incomplete duplicate peer contexts from the lockfile. Fixes pnpm/pnpm#14840.
GitHub Actions updates now stop if an action reference changes while its versions are being resolved. Unrelated workflow edits are preserved.
GitHub Actions homepage links no longer expose server credentials. GitHub server URLs now require HTTPS, with HTTP allowed only for loopback hosts.
pnpm licenses list now reports the runtime downloaded through devEngines.runtime with onFail: "download". The command previously failed with ERR_PNPM_UNSUPPORTED_PACKAGE_TYPE#14172.
pnpm no longer creates a project pnpm-lock.yaml when devEngines.packageManager.onFail is download and lockfile writing is turned off with lockfile: false or --no-lockfile. pnpm still switches to the pinned version #14728.
A registry or @scope:registry set in an .npmrc now wins over the registry a pnpm login credential stored in the global config.yaml points at. Previously, after logging in to one registry, installs in a project whose .npmrc named a private registry went to the logged-in registry instead. They now go to the registry the .npmrc names #14614.
A patch that gives a dependency a preinstall, install, or postinstall script, or a binding.gyp, now runs that build. pnpm asks for build approval first, so the package is listed under "Ignored build scripts" until it is allowed to build. pnpm 12 ran nothing, and pnpm 11 ran it without asking #14648.
Registries that share a host but differ by URL path — one JFrog Artifactory, Nexus, AWS CodeArtifact or GitLab Packages instance serving several repositories — now get a metadata cache directory each. Previously they shared one, so resolving a package from one of them could answer with another's versions, integrity hashes and tarball URLs and fail with ERR_PNPM_TARBALL_URL_MISMATCH#13558.
The URL scheme is part of the cache directory name too, so an http registry can no longer hand its metadata — which can be rewritten in transit — to a resolution configured for https at the same host.
The first install after upgrading refetches registry metadata once. The package store is untouched.
pnpm cache view now labels each entry with the full registry URL. It printed registry.npmjs.org before and prints https://registry.npmjs.org/ now.
pnpm cache list-registries and pnpm cache list print the new directory names. Scripts that parse either command need updating.
Updated the embedded Node.js release keys to the current canonical nodejs/release-keys list.
pnpm sbom now omits package author fields when the manifest author name is empty or contains only whitespace pnpm/pnpm#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. The timestamp carried fractional seconds, which strict SPDX consumers rejected #14684.
Windows filesystem operations now retry permission errors for up to one second. Permanent permission errors previously delayed failure by a minute. Sharing and lock violations retain their one-minute retry budget pnpm/pnpm#14682.
pnpm now writes node_modules/.package-map.json only when nodeExperimentalPackageMap is enabled. Nothing reads the file without that setting. An install that stops writing the map removes the one a previous install left.
pnpm now unpacks a downloaded runtime archive into a randomly named directory inside the store. It previously used a predictable path, where another user of a shared store could plant a symlink and redirect the write outside the store (GHSA-vwc7-r8mq-g2x9).
Catalogs can now resolve workspace dependencies through the workspace: protocol.
pnpm remove and pnpm update now accept --trust-lockfile, --no-trust-lockfile, --trust-policy, --trust-policy-exclude, and --trust-policy-ignore-after. pnpm remove checks the whole lockfile against the active policies unless --trust-lockfile is set.
Added pnpm change check for CI validation of package versions against the versioning.epics bands and versioning.fixed groups in pnpm-workspace.yaml.
Patch Changes
Fetch and tarball errors and retry logs now hide URL credentials, query strings, and fragments that could expose secrets.
Fixed a race during config dependency updates that could redirect a lockfile write through a symlink #14322.
pnpm add --allow-build=!<pkg> now correctly denies builds, including in global installs. pnpm approve-builds <pkg> and pnpm approve-builds !<pkg> now save decisions even when the package is not awaiting approval, with a warning #14067.
Fixed pnpm audit --fix failing without a value or when followed by another flag. pnpm audit --fix=override now respects saveExact and savePrefix when writing overrides #13261, #11523.
pnpm audit now excludes ignored advisories from vulnerability totals and severity counts, and reports them separately #14535.
pnpm deploy no longer requires injectWorkspacePackages. If a workspace dependency's peer has multiple possible versions, deployment reports ERR_PNPM_DEPLOY_AMBIGUOUS_PEER with the conflicting versions. Pin the peer with overrides to deploy without injection #9386.
Fixed concurrent installs sharing a store occasionally failing with an ENOENT error while importing a package file #14353.
Fixed installation failures when a linked local dependency provides a peer dependency also provided by an ancestor, including with pnpm deploy --legacy.
pnpm install --node-linker=hoisted no longer downloads skipped optional dependencies when node_modules already exists #14139.
Fixed pnpm install rejecting a symlinked lockfile when config dependencies are unchanged. Updates to config dependencies also preserve lockfiles with a byte order mark. Writes through symlinked lockfiles remain blocked #14372.
pnpm install now relinks workspace packages when publishConfig.linkDirectory changes. Frozen installs require the lockfile to be regenerated #14488.
Auto-installed optional peers now satisfy their declared range even when the workspace root uses a version outside that range #13867.
Fixed global virtual store paths for dependency cycles to consistently account for the runtime engine when dependencies have allowed builds #14341.
Standalone installations now preserve the bundled node-gyp files needed to build native dependencies.
Downloaded runtimes are now available to dependency lifecycle scripts during installation.
Node.js downloads from nodeDownloadMirrors now use URL-scoped npm credentials, including bearer tokens, basic auth, and tokenHelper#14334.
Fixed globalDir and globalBinDir handling in global configuration and environment variables, including ~/ expansion. This fixes pnpm add -g failing after pnpm config set -g global-bin-dir#14336.
The JavaScript pnpm can again switch to the project's pinned pnpm version on hosts without a matching native binary. If the requested version requires an unavailable native binary, the error now identifies the unsupported host #13622.
Global pnpm config commands now skip project package manager version switching, allowing authentication to be configured before downloading the pinned version #14463.
pnpm self-update, pnpm with, and automatic version switching no longer wait through registry retries when a configured registry has no signatures and registry.npmjs.org is unavailable #14483.
Fixed argument forwarding on Windows with shellEmulator enabled. Trailing backslashes, line breaks, and literal shell expressions are preserved #14548.
Relative scriptShell paths now resolve from the workspace root. Bare command names such as bash still use PATH#14422.
pnpm import now preserves the project-local lockfile when lockfileDir points elsewhere and restores the destination lockfile on failure. Branch lockfile imports leave the shared lockfile unchanged #14563.
catalogMode and --save-catalog no longer move local paths, tarballs, or workspace:<path> specifiers into catalogs #14437.
--side-effects-cache, --no-side-effects-cache, and PNPM_CONFIG_SIDE_EFFECTS_CACHE now toggle only the local cache, preserving any remote cache configured in sideEffectsCache.
pnpm unpublish now handles registry two-factor authentication challenges through web authentication or a one-time password prompt #14464.
pnpm outdated and pnpm update now follow GitHub Actions references using self-repository syntax, such as uses: $/.github/actions/setup.
Added an opt-in proof of concept that lets installs reuse a dependency's build output across machines, by publishing and restoring signed, organization-scoped artifacts through pnpr instead of running the lifecycle scripts locally.
Configure it with the new remoteSideEffectsCache setting. A workspace names the eligible organization and packages; everything describing the act of signing — publish, keyId, builderId, trustedKeys, privateKey and the provenance fields — is refused in pnpm-workspace.yaml and read from the global config file or the environment instead.
Added macOS and Windows x64 and arm64 support to remote shared build artifacts pnpm/pnpm#13771.
Added the audit.ignorePrune setting. When set to true, pnpm audit --fix removes ignored GHSA entries that no longer appear in the audit report.
Generalized the experimental shared-artifact protocol so candidates and signed payloads identify a discriminated subject. Dependency side effects use package and source-integrity subjects, while workspace tasks use project and task subjects.
This changes shared-artifact request bodies and signed payloads. A pnpr server and its clients have to be on matching versions.
pnpm init now pins the latest pnpm version, instead of the version of pnpm that ran the command. A project scaffolded by an outdated pnpm therefore no longer inherits that staleness through its own devEngines.packageManager / packageManager pin #7490.
The version is read from the latest tag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry, offline, or a latest that the minimumReleaseAge / trustPolicy settings reject — pnpm init pins the running version as before, and never fails or hangs on the lookup. A latest that is older than the running pnpm is never pinned either.
A scope set in a project's pnpm-workspace.yaml is now ignored, with a warning naming where to set it instead. pnpm login records the scope as a @scope:registry route in the machine-global auth.ini, which outranks ~/.npmrc in every project — so a repository-committed file could redirect a scope such as @acme for all of a user's other projects after one routine login. Use --scope, the PNPM_CONFIG_SCOPE environment variable, or the global config file instead #13557.
Verified remote build artifacts are persisted in the shared store with their signed origin metadata. Later installs reverify the artifact against current trust, policy, platform, and source before reuse, while invalid remote variants are quarantined per channel (pnpm/pnpm#13771).
Persist completed recursive tasks so --resume-from skips exactly the work that passed during a matching interrupted or failed pnpm -r run / pnpm -r exec invocation. When no compatible state exists, pnpm retains its graph-based resume behavior.
Allowed pnpm update --patches to refresh registry revisions through a configured pnpr server while retaining locked package versions.
Added explicit registry revision selection with <version>+rN and pnpm update --patches for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries.
Workspace install, rebuild, pack, publish, stage, and lifecycle work now starts as soon as its dependencies finish instead of waiting for an unrelated topological group.
pnpm stage approve now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. The whole batch is approved with a single one-time password, and pnpm asks for a new one only once the registry stops accepting it. Inside a workspace, the selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry.
Added per-task concurrency limits to workspace task orchestration. Set tasks.<name>.concurrency in pnpm-workspace.yaml to limit how many instances of that task may run across workspace projects at once:
tasks:build:concurrency:2
Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the registries setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts.
sideEffectsCache now declares the whole of how a package's build output is reused — whether one is restored, whether one is saved, and the remote tier that shares it between machines:
sideEffectsCache: 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:
^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:
A dependency cycle among the tasks of a run is now an error naming the participating tasks (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.
Under --no-bail, tasks whose dependencies failed are reported as skipped, not failed, and do not add to the exit code.
With --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.
Output is inherited rather than piped only when at most one script can ever be in flight (--workspace-concurrency=1, or the graph forces the scripts to run one after another).
Patch Changes
An _auth entry in the global config file no longer decides which registry packages come from when something else says. A registry or registries declared in pnpm-workspace.yaml or the global config now wins over the route inferred from a stored credential, which still applies where nothing else declares one. The pnpm_config__auth environment variable is unchanged: it stays the way to point a CI runner at a mandated proxy, and still overrides what a repository declares.
Prevent installs through a symlinked node_modules directory from rewriting the target checkout pnpm/pnpm#14286.
Treat empty scripts selected by a regular expression as missing before running dependent tasks.
The options type of the fetch command now declares allowBuilds, a setting its handler already forwarded to the installer. Type-level only — what pnpm fetch does is unchanged.
Filter hidden scripts matched by a regular expression during recursive runs when a visible script also matches.
Fixed automatically switched pnpm versions forcing all descendant pnpm processes to use the same version pnpm/pnpm#14309.
Fixed ERR_PNPM_UNUSED_PATCH validation during incremental installs pnpm/pnpm#13692.
Fixed pnpm deploy --prod failing when an excluded dev dependency was also declared as an optional peer dependency pnpm/pnpm#14302.
pnpm update -g no longer downgrades a global package. --latest resolves the latest dist-tag, which can point at an older release than the one installed — after pnpm add -g <pkg>@next, for instance #14270.
pnpm update -g also no longer changes the pnpm version. pnpm's own global install belongs to pnpm self-update#14270.
Copying a built package to its other hoisted locations no longer replaces the destination directory. With nodeLinker: hoisted, that replacement deleted the dependencies nested inside the destination's node_modules, and made concurrent copies of the same build chunk fail with ERR_PNPM_ENOENT: no such file or directory, rename '.../node_modules/_tmp_...'#12880.
pnpm update no longer replaces the specifier a project declares for a dependency that is also listed in overrides. A catalog: reference stays a catalog: reference, and a declared range stays as written, instead of being rewritten to the version the override resolved to #12115.
pnpm update no longer moves the range a project declares for a dependency that overrides also lists, even when the override repeats that range verbatim. Previously the updated package.json disagreed with the lockfile, so the next pnpm install --frozen-lockfile failed with a specifier mismatch #14224.
Make pnpm add --lockfile-only skip dependency linking pnpm/pnpm#14286.
--production is accepted again as an alias of --prod on install, fetch, prune, update, list, why, and sbom, and the install that verifyDepsBeforeRun reproduces is now spelled with --prod. pnpm run no longer aborts with "unexpected argument '--production' found" after a production-only install #14147.
The progress output no longer overwrites the lines above it once it grows taller than the terminal window #14270.
Restoring a dependency's build from the remote side-effects cache no longer downloads files the store already holds.
Forward patchedDependencies hashes and packageExtensions to pnpr so server-side resolution preserves patches and package extensions in the lockfile and installed packages.
Published the workspace task graph and scheduler as @pnpm/workspace.task-scheduler so other workspace commands can use the same dependency-aware scheduling as recursive run and exec.
The environment variables for the remote side-effects cache are named for the setting they configure: PNPM_SIDE_EFFECTS_CACHE_REMOTE_KEY_ID, ..._BUILDER_ID, ..._IMAGE_DIGEST, ..._ARCHITECTURE_BASELINE, ..._PRIVATE_KEY, ..._BUILD_ENV, ..._TRUSTED_KEYS and ..._PUBLISH. The PNPM_REMOTE_SIDE_EFFECTS_CACHE_* names keep working, and the new one wins when both are set.
A devEngines.packageManager range pin on pnpm is now recorded in pnpm-lock.yaml's packageManagerDependencies when the running pnpm already satisfies it, using the running version and keeping the range as the recorded specifier. Previously only an exact pin — or a range resolved on the way through a version switch — reached the lockfile, so a range pin written by hand (or by any tool other than pnpm add / pnpm self-update) left the project without the shared resolution the pin exists to provide.
Fixed recursive run cleanup on Windows when a lifecycle script fails while another script's process tree is still running.
The update notification now suggests pnpm self-update when PNPM_HOME manages the pnpm in use, and the standalone install script otherwise — under Corepack, or when another package manager installed pnpm. pnpm self-update under Corepack names the standalone install script too.
Enforce allowBuilds when a prepared git dependency is reused from the shared store, and use the lockfile's canonical git resolution ID in approval suggestions.
Topologically sorting workspace projects now runs in linear time, fixing installs and lockfile updates that stalled for seconds on workspaces with thousands of projects forming deep dependency chains #14149, #14151.
Fixed pnpm v11 incorrectly reporting confirmModulesPurge as unrecognized when set in pnpm-workspace.yaml. The Rust CLI now identifies the unsupported option as a pnpm v11 setting instead of suggesting an unrelated setting.
pnpm install --frozen-lockfile no longer fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE when the pinned pnpm version recorded in pnpm-lock.yaml has to be re-resolved before it can be installed. It runs the pnpm version the lockfile pins and leaves the lockfile unchanged #14124.
Under nodeLinker: hoisted, peer-resolution variants of an injected directory dependency (a file: snapshot) are materialized as separate copies again instead of collapsing onto the first-seen variant. Each copy keeps its own peer-resolved dependency set, so a project pinning one peer version no longer resolves another project's variant — Bit root components with conflicting peers across injected copies rely on this.
Fixed pnpm install --merge-git-branch-lockfiles --frozen-lockfile failing with ERR_PNPM_OUTDATED_LOCKFILE when a branch lockfile predates the removal of a dependency, or its move to another dependency group #13966. A dependency that no project declares anymore is no longer reinstated by the merge, and the packages it was the only path to are dropped with it.
Batch workspace publishing accepts a shared scope-specific credential, rejects mismatched credentials for a registry before publishing, and runs the publish and postpublish scripts after each completed registry group pnpm/pnpm#14101.
The Rust CLI now honors five settings it recognized but ignored: updateNotifier, legacyDirFiltering, initAuthorName / initAuthorEmail / initAuthorUrl, initLicense, and initVersion. pnpm install and pnpm add check once a day for a newer pnpm and print how to get it (turn it off with updateNotifier: false); a {<dir>} filter selector can go back to matching the subtree below the directory with legacyDirFiltering: true; and pnpm init writes the configured author, license, and version into the package.json it scaffolds. PNPM_CONFIG_INIT_VERSION is now read as well.
maxsockets, npm's spelling of maxSockets, is no longer ignored: both spellings are read from pnpm-workspace.yaml, the global config file, the environment, and the command line, in that increasing order of precedence — a value passed on the command line now wins even when the two sides spelled the setting differently.
A lastUpdateCheck timestamp dated in the future — after a clock change, a restored snapshot, or a hand-edited state file — no longer silences the update check until that time comes around.
legacyDirFiltering no longer reaches the workspace-root selectors pnpm generates for itself: the !{<workspace-root>} exclusion a recursive run / exec / add / test appends, and the {<workspace-root>} inclusion --workspace-root appends. Read as subtree matches they named every project below the root, so a recursive command under the setting selected nothing at all, and --workspace-root pulled in every project below the root instead of the root alone #14101.
pnpm install --frozen-lockfile no longer fails when pnpm-lock.yaml records the pinned pnpm version alongside an engine package the running pnpm does not install it from. An entry pinning another version is still refused, and a plain install rewrites the block #14124.
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.
The registry and @scope:registry entries show the merged routes rather than raw .npmrc values, so they always agree with the registries view.
Settings that no supported pnpm version recognizes get their own warning. A key in the global config file that this version of pnpm does not read is no longer reported with advice to move it to a project-level pnpm-workspace.yaml (where it would be ignored too); the warning now says the setting is not recognized by this version of pnpm, names the pnpm version that does read it when there is one (for example, globalShims is a pnpm v12 setting), and suggests the closest real setting name when the key looks like a typo. Unrecognized and non-camelCase keys in a project's pnpm-workspace.yaml, previously ignored silently, are now reported the same way. pnpm config get <key> and pnpm get <key> no longer print config-load warnings, so a script capturing the value gets the value alone.
The importPackage pnpmfile hook is deprecated. pnpm now prints a warning when a pnpmfile defines it, and the hook will be removed in the next major version. It also opts the installation out of the parallel package importer, making installation slower. If you rely on this hook, comment on #14101.
node_modules/.modules.yaml no longer records the registries an install resolved from, and the recorded copy is dropped from the file on the first install that rewrites it.
It dated from the lockfile format that spelled a dependency's path relative to its registry, where reading an installed tree meant knowing the registries it was installed with. Dependency paths have not carried a registry for several major versions, and the recorded copy outlived its use: pnpm list, pnpm why, and single-project installs preferred it over the project's own configuration, so a project whose registry had changed since its last install was still read through the old one.
They now use the configured registries, like every other command already did.
When enableGlobalVirtualStore is on, every process pnpm spawns for the project (pnpm run, pnpm exec, lifecycle scripts) now receives a NODE_PATH pointing at the project's hoisted node_modules, plus a NODE_OPTIONS--import flag that registers a resolve hook restoring NODE_PATH lookups for ESM imports. Dependencies that import undeclared ("phantom") packages keep resolving under the global virtual store — for both CommonJS and ESM — without installing the @pnpm/plugin-esm-node-path config dependency pnpm/pnpm#9618. Tools run by pnpm dlx resolve such dependencies too: the JS CLI passes them the same environment, while the Rust CLI's dlx cache is self-contained, so its layout already exposes them.
A registry can now declare that its abbreviated metadata carries the time field, so resolutionMode: time-based reads the full metadata document only from the registries that need it:
registry.npmjs.org omits time from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: registrySupportsTimeField answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a time field npmjs does not serve. The answer is now per registry, and registrySupportsTimeField remains the answer for every registry that does not declare one.
The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf.
A pnpr resolve request now carries the client's registries the way the registries setting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one's serverType — in place of the prefix map it used to send.
The server routes them through the same inversion the config reader runs, so a pnpr-served install resolves a scoped dependency from the registry that scope is routed to, which it previously could not: only the default registry and the prefix-addressed ones reached the server. A declared serverType reaches it too, so the tarball URLs pnpr omits from the lockfile match the ones the client reconstructs.
Built-in scope routes the project has not pointed elsewhere are not declared, so a pnpr server's allowlist is not asked about npm.jsr.io on requests that resolve no JSR package.
A registry a request only declares is no longer refused up front for being off the server's allowlist — a client describes its whole configuration, including scopes a given resolve never reaches, so a stray @scope:registry in a developer's ~/.npmrc no longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message.
This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned.
The registries setting now declares a registry once, keyed by its URL, with everything about that registry in the entry: how it lays out tarball URLs, the scopes routed to it, and the bare-specifier prefix it answers to.
serverType tells pnpm how the registry lays out its tarball URLs, which decides whether a URL can be omitted from pnpm-lock.yaml:
undeclared (the default) — strict. Only the exact canonical URL is treated as reconstructible.
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.
Migrating
The older registries shape, a map of <scope>: <url> strings, still works and needs no change:
registries:'@acme':https://npm.acme.example/
namedRegistries is deprecated in favor of the prefix field, and is still read for prefixes registries does not declare.
toLockfileResolution and isCanonicalRegistryTarballUrl now take their registry and layout as an options object rather than positional arguments, so @pnpm/lockfile.utils and @pnpm/resolving.tarball-url get a major bump.
An install that had to re-hash store files to verify them now reports it. If that cost more than a second, it says how long — The integrity of N files was checked in 2.5s. — and if it was quick but covered more than a thousand files, it names the cause instead: their timestamps changed since the store recorded them, which a backup tool, an antivirus scan or a copied store can do.
Added virtualStoreType, which names where the virtual store lives — one store per machine, or one per project:
virtualStoreType:global # or: project
It is the canonical spelling of enableGlobalVirtualStore, which keeps working. When a project sets both, virtualStoreType wins. It can also be set through PNPM_CONFIG_VIRTUAL_STORE_TYPE and read back with pnpm config get virtualStoreType. The default is unchanged — project, so the shared store stays opt-in.
The setting is independent of nodeLinker. isolated and pnp both work with either store type, and hoisted writes no virtual store at all, so it is unaffected.
Patch Changes
pnpm add --allow-build now adds to the allowBuilds entries already in pnpm-workspace.yaml instead of replacing them #13872.
Kept pending build approvals available after removing an unrelated dependency.
pnpm approve-builds now removes onlyBuiltDependencies, onlyBuiltDependenciesFile, neverBuiltDependencies, and ignoredBuiltDependencies from pnpm-workspace.yaml when it writes allowBuilds. Those settings were replaced by allowBuilds in pnpm 11 and silently ignored since, so a workspace migrated from pnpm 10 kept them around looking active.
pnpm audit no longer reports a patched version that was never published or is deprecated. The inferred patched range (e.g. >=4.17.24 from <=4.17.23) is now checked against the registry packument, and the report is corrected to the lowest non-deprecated published version that satisfies it (e.g. >=4.18.1 when 4.17.24 does not exist and 4.18.0 is deprecated). When no published version satisfies the range, the report shows Patched versions: None. This also prevents pnpm audit --fix from adding overrides or minimumReleaseAgeExclude entries for patches that do not exist #13824.
pnpm audit --fix and pnpm audit --fix update no longer add a minimumReleaseAgeExclude entry when the registry packument shows that the minimum patched version was never published. Previously such entries were written for versions that do not exist, which would have let a later publish of that version bypass the minimumReleaseAge gate #11563.
The --json output of pnpm audit now returns patched_versions: null for advisories whose inferred patch is not available (never published, skipped, yanked, or deprecated), making it easier for tooling to distinguish "no fix available" from "fix available at version X".
Fixed pnpm patch-commit in project and edit paths containing non-ASCII characters.
The package and bump pickers of pnpm change now size their page from the terminal height instead of always showing 7 rows. They fall back to 7 rows when the terminal height is unknown pnpm/pnpm#13815.
Canceling a pnpm change prompt with Ctrl-c no longer prints a stack trace. It reports Change canceled and exits with a success status, like the other interactive commands #13814.
Re-fetch full registry metadata when minimumReleaseAge is enabled and an abbreviated packument's time map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version pnpm/pnpm#13741.
A config dependency carrying an inline integrity (the <version>+<integrity> form, or the object form without a tarball) now takes its tarball URL from the registry's packument instead of deriving it from the registry URL, so migrating one costs an extra metadata request. On a registry that serves tarballs from a path pnpm cannot derive, GitLab's group endpoint for one, installing such a config dependency failed with a 404 while the same package installed fine as a regular dependency #13765.
Fixed PNPM_CONFIG_NODE_VERSION being ignored when setting the Node.js version used for compatibility checks.
A custom fetcher can no longer replace the archive integrity that pnpm-lock.yaml pins: the locked value is restored after a canFetch or fetch hook rewrites the resolution, and delegating a locked archive to a directory or git source now fails instead of installing unverified content.
The Rust CLI now also loads the pnpmfiles named by the pnpmfile setting (a single path or an ordered list), and hands custom fetchers native localTarball and remoteTarball callbacks — including on a fresh install that has to compute a missing tarball integrity, which is then reused by later offline installs. File maps a fetcher returns are accepted only when they match what those native callbacks extracted.
Fixed an issue where running pnpm dedupe --check in projects with nodeLinker: hoisted would cause dependencies to be moved out of node_modules into node_modules/.ignored.
pnpm deploy --prod and pnpm deploy --no-optional no longer list the excluded dependency groups in the deployed package.json and pnpm-lock.yaml. The deployed lockfile referenced packages that the deploy left out of its graph, so installing in the deploy directory afterwards created dangling symlinks #13623.
Don't treat files like license16.json as a package license when deciding if the workspace LICENSE file should be included in the packed package.
pnpm exec --recursive --no-reporter-hide-prefix no longer prints a blank prefixed line after each chunk of a command's output, and no longer splits a line in two when it straddles a chunk boundary.
Fixed 404 errors when installing from a registry that serves scoped packages only from a percent-encoded path, such as GitHub Enterprise Server. Outside registry.npmjs.org, a tarball URL that encodes the scope separator as %2f or %2F is no longer mistaken for one that pnpm can rebuild from the package name, version, and registry, so it is kept in pnpm-lock.yaml and requested verbatim on the next install #13534.
Fixed trustPolicyExclude and minimumReleaseAgeExclude being ignored when set to a single string instead of a list. The value was read one character at a time, so the exclusion never matched the package it named — and a * anywhere in it matched every package, silently switching the policy off.
pnpm init now pins the exact pnpm version instead of a ^ range, and records it in the packageManager field alongside devEngines.packageManager. Corepack reads only packageManager and accepts nothing but an exact version, so it rejected the generated package.json with "expected a semver version" pnpm/pnpm#13969. A package created inside an existing workspace is still left unpinned — it follows the pin at the workspace root — and --no-init-package-manager still scaffolds a manifest without any pin. In pnpm 12, pnpm init also honors initType and its --init-type flag, so the manifest it writes is the same one pnpm 11 writes.
Fixed an issue where package overrides were written into the metadata cache, causing removed overrides to keep applying on subsequent installs pnpm/pnpm#13918.
On Windows, upgrading pnpm no longer leaves a stale pnpm.ps1 behind. PowerShell resolves pnpm.ps1 ahead of pnpm.cmd, so a shim written by an older installation kept running the previous version. Linking the pnpm CLI's bins now deletes it #13919.
Fixed an inconsistency where minimumReleaseAgeExclude (and trustPolicyExclude) wildcard/bare-name rules behaved differently in the evaluator and normalizer. A bare rule now consistently evaluates as matching every version, preventing unexpected behavior and silent widening of version policy exemptions when pnpm rewrites the workspace manifest pnpm/pnpm#13725.
A frozen install no longer rewrites the packageManagerDependencies block of pnpm-lock.yaml. When the pnpm version pinned by devEngines.packageManager (or by packageManager) is missing from the lockfile or no longer matches it, --frozen-lockfile now fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE instead of resolving the version and saving it, so a manifest whose pin was bumped without regenerating the lockfile can no longer pass CI #14009.
A git dependency installed over HTTPS from a hosted repository now keeps its branch, tag, or version range in the specifier recorded in package.json. It was written back without one, so the next pnpm update moved the dependency to the repository's default branch #13999.
Fixed pnpm update --global --latest failing with a 404 error when a globally installed package was not added from the registry by name. Packages installed from a local path (link:/file:), a git repository, a tarball URL, an npm: alias, or a named registry now keep their spec during a global update instead of being looked up by name in the default registry. See #12854.
Fix recursive pnpm update <name>@<version> so an exact pinned update stays scoped to the requested version line: copies of the same package on another major line — or, for a 0.x request, another minor line — keep their locked resolution instead of being re-resolved along with the target.
Under nodeLinker: hoisted, a dependency declared against a peer-resolution variant of a package version is no longer dropped from the installed layout. All variants of a version share one hoisted copy, and edges pointing at any of them now resolve to it, so the depending project keeps the package in its .package-map.json and the depending package keeps it in its node_modules/.bin.
Fixed pnpm install --merge-git-branch-lockfiles deleting the per-branch lockfiles when the lockfile setting is false. Such an install never reads them, so it has nothing to merge them into and now leaves them alone.
Fixed pnpm install sometimes not exiting after printing Done in Xs#12297.
Fixed pnpm failing to read .modules.yaml files containing long dependency paths #13875. The manifest is now parsed as JSON (the format pnpm writes it in), falling back to the YAML parser only for manifests written by old pnpm versions.
With preferSymlinkedExecutables, NODE_PATH again points at the virtual store of the workspace root when pnpm is run from inside a workspace package, so scripts can resolve dependencies that live only in the hoisted store #13912.
Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win pnpm/pnpm#13976.
pnpm pkg get and pnpm pkg set now accept hyphens inside a dot-notation property path, so pnpm pkg get dependencies.some-package-name reads the key instead of failing with ERR_PNPM_UNEXPECTED_TOKEN_IN_PROPERTY_PATH. The bracketed and quoted forms already worked and are unchanged.
A resolve request now carries the client's resolutionMode, so an install delegated to a pnpr server picks versions the way the client would. time-based and lowest-direct reached the server as nothing at all, leaving it on its highest default: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored.
This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving highest; the protocol is still experimental and unversioned.
Fixed pnpm installs using pnpr to honor the client's autoInstallPeers, dedupePeers, and excludeLinksFromLockfile settings pnpm/pnpm#13389.
pnpm remove now prunes undecided entries ("set this to true or false") from allowBuilds in pnpm-workspace.yaml when sharedWorkspaceLockfile: true and the corresponding packages are removed pnpm/pnpm#13892.
Fixed workspace discovery for pnpm-workspace.yaml files without a packages field so commands only consider the workspace root instead of recursively scanning nested projects #14047.
A runtime installed through devEngines.runtime now matches the host when supportedArchitectures lists several platforms. Listing os: [darwin, linux] and cpu: [x64, arm64] used to install the runtime built for the first entry of each list, so a machine running Linux on arm64 got a macOS x64 Node.js that could not execute #13898.
pnpm sbom now fails with ERR_PNPM_SBOM_MISSING_IMPORTERS when pnpm-lock.yaml has no entry for a selected project, instead of writing an SBOM that under-reports that project's dependencies. Previously this crashed with Cannot read properties of undefined (reading 'devDependencies').
pnpm self-update now rewrites a simple devEngines.packageManager.version range (^/~) to the newly installed version, keeping the operator — matching how pnpm update and pnpm runtime set rewrite ranges. Complex ranges such as >=8.0.0 that the new version satisfies are still left unchanged #13935.
pnpm self-update <tag> no longer downgrades when the dist-tag points at the pnpm version already running and that version is younger than minimumReleaseAge. The maturity cutoff moved the tag back to the previous mature release, so pnpm self-update next-12 on v12.0.0-rc.4 switched to v12.0.0-rc.3.
pnpm set-script now updates package.json instead of failing with ERR_PNPM_NOT_IMPLEMENTEDpnpm/pnpm#13956.
pnpm update now preserves the existing range operator when updating a prerelease dependency. See #7002.
Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges ("*", "^1.2.3") rather than the workspace: protocol. With preferWorkspacePackages enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install.
Added fetchWarnTimeoutMs and fetchMinSpeedKiBps to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters pnpm/pnpm#12042.
An override change is now absorbed by the fast lockfile update even when another, unchanged override uses the catalog: protocol. Previously any catalog:-valued override forced a full re-resolution whenever the override list changed, which could move unrelated packages in the lockfile (for example after pnpm audit --fix added an override).
Packed workspace package manifests now preserve dependency order, making repeated pnpm pack output deterministic #10167.
pnpm update <name>@<version> now fails with ERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEP when the package is not a direct dependency of any selected project, instead of quietly updating it to whatever a fresh install would resolve. There is nowhere to record the version in that case, so the request cannot be honored, and the error points at the overrides entry that does pin a transitive dependency. Ranges and tags are unaffected, and a package that any selected project declares directly still takes its version as before.
trustPolicy: no-downgrade no longer aborts the install with ERR_PNPM_MISSING_TIME on registries that serve no per-version time field when minimumReleaseAgeIgnoreMissingTime is set. The trust check reads the same publish dates the minimumReleaseAge check does, so it now honors the same opt-in and skips the affected package with a warning #12446.
minimumReleaseAgeIgnoreMissingTime no longer lets a lockfile entry the registry does not list pass the minimumReleaseAge check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure.
The missing-time warning now names the check it is reporting on, so a package whose minimumReleaseAge and trustPolicy checks are both skipped warns about both instead of only the first.
pnpm update <pkg>@<version> now updates only the selected packages and leaves unrelated dependencies unchanged. A selector that renames the package it installs — pnpm update <alias>@npm:<pkg>@<version> or the jsr: equivalent — now targets the package the alias installs rather than the alias.
Fixed verifyDepsBeforeRun being ignored when set to install, warn, error, or prompt through the PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN environment variable or the --config.verify-deps-before-run flag #13816. Only the boolean values were accepted before, so a string value was silently dropped.
pnpm version <bump> with --dry-run no longer edits package.json files. It now only reports the bumps it would make, and skips the working tree check, the version lifecycle scripts, the commit, and the tag pnpm/pnpm#13953.
Added pnpm cache path, which prints the directory pnpm uses for its metadata cache. CI setups can use it to cache that directory — including the lockfile verification log, which lets a job skip re-checking an unchanged lockfile against the configured supply-chain policies.
--config.config-dir no longer reaches the config through a project's pnpm-workspace.yaml, and neither do the --config. spellings of the other settings a project manifest may no longer contribute (--config.pnpm-home-dir, --config.workspace-dir, --config.global-pkg-dir, --config.root-project-manifest-dir). None of them was ever a supported way to set those directories: pnpm resolves them from the environment, and these flags took effect only because the project-manifest merge re-applied the command line afterwards. The dedicated flags, such as --dir and --global-dir, are unaffected #13629.
pnpm config set refuses to write a setting to a project's pnpm-workspace.yaml that pnpm does not read from there, rather than leaving a key in the file that does nothing. Those settings are configDir, pnpmHomeDir, stateDir and the others that name machine-level state. The command fails with ERR_PNPM_CONFIG_SET_NOT_A_PROJECT_SETTING, naming where the setting does belong when it belongs somewhere. pnpm config delete still clears one that a file already carries, in whichever spelling it uses #13629.
Added a new setting minimumReleaseAgeExcludePrune. When enabled, pnpm add, pnpm update, and pnpm remove prune the entries of minimumReleaseAgeExclude in pnpm-workspace.yaml that the freshly written lockfile no longer resolves: versions that are gone are dropped (an entry is removed once none of its versions remain), and entries for packages that are no longer in the lockfile are removed too. Name patterns (@scope/*) are always kept. The cleanup is skipped when the install's lockfile does not cover the whole workspace (sharedWorkspaceLockfile: false), since entries another project still needs would look stale.
Renamed cleanupUnusedCatalogs to catalogPrune, so that catalog pruning and release-age exclude pruning use one vocabulary. cleanupUnusedCatalogs continues to work; when both are set, catalogPrune wins.
A project's pnpm-workspace.yaml can no longer choose where pnpm keeps its credentials, its own installation, or the registry it downloads its next version from. One of those settings is configDir, which decided where pnpm login writes the granted token. bin, dir, globalBinDir, globalDir, npmrcAuthFile, pnpmHomeDir, stateDir, userconfig and workspaceDir are ignored there now too, and pnpm warns about the ones it finds. cacheDir and storeDir are unaffected #13629.
Resolving a Node.js runtime version (devEngines.runtime / runtime: specifiers) is now much faster: the per-version release metadata is cached in the pnpm cache directory after its signature is verified, and an exact stable version such as runtime:22.23.2 no longer downloads the Node.js release index. A pinned runtime whose metadata was fetched once resolves without any network access, which removes the noticeable delay on the first node invocation in a project pinning an already-downloaded runtime #13899.
Patch Changes
Fixed intermittent ERR_PNPM_ENOENT and ERR_PNPM_ENOTEMPTY errors while renaming _tmp_* directories during installation with nodeLinker: hoisted, in workspaces that also use patchedDependencies.
pnpm add no longer re-resolves the dependency graph when pnpm-lock.yaml already holds a version satisfying the request — promoting a transitive dependency to a direct one, or adding to a second workspace package what a first one already depends on, now only saves the dependency in package.json and records its importer entry. A satisfying locked version is necessary but not sufficient: the install still falls back to a full resolution for a dist tag, an alias, a workspace:/catalog:/git/tarball specifier, --save-peer, an overridden package, a catalogMode other than manual, and — under resolutionMode: time-based or lowest-direct, which resolve a direct dependency to the low end of its range — a range several locked versions satisfy.
Global installs now switch over atomically. The command shims in the global bin directory point at a stable per-package link rather than at the directory a particular install produced, so pnpm add -g and pnpm update -g activate a new version by moving that one link instead of rewriting every shim. A command can no longer be missing from PATH while an install is in progress, and a failed install leaves the previous version in place.
pnpm audit --fix and pnpm audit --fix update no longer add minimumReleaseAgeExclude entries for patched versions that were published before the minimumReleaseAge cutoff. The publish time of each minimum patched version is now checked against the registry metadata, and only versions young enough to be blocked by the age gate get an exclusion entry #11563.
pnpm add <pkg>@<version> and pnpm update <pkg>@<version> under a non-manual catalogMode now move the catalog entry's resolution to the requested version. Previously, when the catalog entry was a range that covered the requested version but resolved to a different one, the request was dropped silently: nothing was installed, nothing was written, and no error was raised.
A project that wasn't part of an install that moved a catalog entry now follows the entry the next time it is installed. It used to keep the version the entry resolved to before — a version the entry no longer allowed — and no later install corrected it, so one catalog entry ended up resolved to two versions.
pnpm add <pkg>@<version> and pnpm update <pkg>@<version> under catalogMode: strict no longer fail with ERR_PNPM_CATALOG_VERSION_MISMATCH when the catalog entry is a range that the wanted version satisfies. The dependency keeps using the catalog; only a version that really falls outside the catalog's range is rejected #13715.
A changed catalogs or pnpm.overrides block no longer has to be the only change for pnpm install to update the lockfile in place. Editing an override while also removing a dependency, or changing a catalog entry in the same commit as a range bump, is now absorbed in one pass instead of re-resolving the whole dependency graph #13799.
Fixed the lockfile an in-place override update wrote when the overridden package was also a catalog entry: the entry kept the version it had before the override moved the package. The same could happen in reverse, when a catalog entry moved a package an override pins. Both cases now re-resolve instead.
pnpm install now updates the lockfile in place even when several kinds of changes happened since the last install — for example a removed dependency together with a widened ignoredOptionalDependencies list, or a dependency edit alongside a patch or settings change. Previously any combination of changes forced a full re-resolution #13763.
pnpm deploy injects workspace dependencies again, so the deploy directory is self-contained instead of symlinking back into the source workspace #13754. Enabling injectWorkspacePackages with dedupeInjectedDeps disabled now also rewrites already-linked workspace dependencies to injected copies.
pnpm deploy --no-optional no longer writes a lockfile whose snapshots reference optional dependencies that the deploy excluded.
Removing the last dependency that references a catalog entry via the fast lockfile update no longer leaves the stale catalog entry in pnpm-lock.yaml.
A git dependency whose clone (or shallow fetch) fails now reports which package it belongs to, under the ERR_PNPM_GIT_FETCH_FAILED code, with credentials in the repository URL redacted. When the lockfile records an SSH remote, the error also explains that fetching it needs an SSH key for that host, and that a lockfile entry written before pnpm v11.21 can be re-recorded over HTTPS with pnpm update <package>#13743.
An integrity recorded on a git dependency's resolution (resolution: {type: git, repo, commit, integrity: sha512-…}) is no longer treated as a checksum. pnpm never verifies a git checkout against such a hash — the commit pins the content — so it is now dropped when the lockfile is rewritten, and pnpm sbom no longer republishes it as a CycloneDX/SPDX checksum. Lockfiles carrying one also load again instead of failing with ERR_PNPM_BROKEN_LOCKFILE#13042.
pnpm sbom now also publishes the checksum of a type: binary runtime archive, which pnpm does verify.
A git dependency whose git ls-remote fails now reports the ERR_PNPM_GIT_RESOLVE_FAILED code, naming the dependency instead of printing a bare git invocation, with credentials in the repository URL redacted. A specifier that does not ask for SSH resolves over HTTPS, because the URL recorded in the lockfile has to work on every machine that installs it, so the error explains how to substitute the transport on a machine that can only reach the host over SSH (git config --global url."git@<host>:".insteadOf "https://<host>/") #13743.
A missing git executable is reported as one, instead of surfacing the raw failure to start the process.
Credentials embedded in a git specifier are redacted from the "Could not resolve <ref> to a commit of <repo>" errors too.
Resolving a public repository makes one git ls-remote round-trip instead of two.
pnpm install after moving a dependency between dependencies, devDependencies, and optionalDependencies now updates the lockfile in place instead of re-resolving the whole dependency graph #13696.
syncInjectedDepsAfterScripts no longer fails with ERR_PNPM_UNSUPPORTED_INODE_TYPE when a workspace package contains an inode that is neither a file nor a directory, such as the FIFO 1Password's environments create for .env. Such an inode cannot be hardlinked into the injected copy, so it is skipped and the rest of the package still syncs #13550.
syncInjectedDepsAfterScripts also no longer fails with EEXIST when a workspace package replaced a file with a directory of the same name since the injected copy was last synced.
syncInjectedDepsAfterScripts no longer fails with ENOTDIR when a workspace package replaced a directory with a file of the same name and the injected copy still held that directory's contents.
syncInjectedDepsAfterScripts now removes the bin link of a bin the script dropped. Previously only new bins were linked, so a build step that stopped declaring one left its shim behind, pointing at a command that was no longer there.
syncInjectedDepsAfterScripts now identifies a file by its device as well as its inode number. An inode number is only unique within one filesystem, so on its own it could match an unrelated file on another device and leave that path stale in the injected copy.
pnpm store prune no longer deletes the lockfile verification log. The log records which lockfile passed which supply-chain policies, so it stays valid across a prune of the store; keeping it lets the next install skip re-verifying an unchanged lockfile.
Widening a dependency's range no longer leaves the project on an older version. The lockfile update now points the project at the highest version of that dependency already in the lockfile that satisfies the new range — matching what a full resolution records — instead of keeping the locked version whenever it happened to satisfy, which could leave a duplicate behind. A range change that only an already-locked version satisfies is now also handled without re-resolving #13778.
resolutionMode is no longer ignored when minimumReleaseAge is in effect. lowest-direct and time-based pick the lowest satisfying version of a direct dependency again; previously any active release-age cutoff — including the built-in default — silently forced the highest, so resolutionMode only worked when minimumReleaseAge: 0 was set explicitly #13752.
Adding a package to a workspace no longer forces a full re-resolution when every dependency it declares is already locked for a sibling. The lockfile update writes the new project's importer entry from the versions the lockfile already holds; a dependency no locked version satisfies still reaches the resolver #13696.
pnpm config delete <key> no longer fails with ENOENT when the config file it would edit does not exist. Clearing a setting that was never set is a no-op #13651.
Changing a pnpm.overrides entry to a version range now updates the lockfile in place when a version the lockfile already holds satisfies the range, instead of re-resolving the whole dependency graph. Only exact versions were handled before #13696.
Changing a parent-scoped pnpm.overrides entry ("parent>child": "2.0.0") now updates the lockfile in place instead of re-resolving the whole dependency graph. Only the named parent's dependency moves; every other package keeps the version it had #13795.
Removing a dependency, or moving one to another already-locked version, no longer re-resolves the whole dependency graph just because some package resolves a peer with the same name. The lockfile update now compares the peer suffixes against the exact name@version the removal severed, so a suffix that names a different — still present — version of that dependency is left alone #13781.
Projects with a pnpmfile now use the fast lockfile update paths: an unchanged pnpmfile (proven by the recorded pnpmfileChecksum) no longer forces a full re-resolution for removals, dependency group moves, compatible range changes, and the other in-place lockfile rewrites #13696.
A lockfile entry whose resolution is unchanged no longer loses its recorded deprecated marker when a registry serves the package's metadata inconsistently — re-resolving to the same version keeps the deprecation instead of silently dropping the line #13846.
pnpm prune is now recursive by default inside a workspace, just like pnpm install. This fixes pnpm prune --prod in a workspace root emptying the node_modules directories of the other workspace projects, dropping the links to the workspace packages they depend on in production #13718.
A setting written in kebab-case in the global config.yaml is now reported instead of being silently ignored #13650.
pnpm remove no longer re-resolves the dependency graph. The removed dependency's entries are dropped from pnpm-lock.yaml and anything they made unreachable is pruned, without registry access. The install still falls back to a full resolution when a surviving package resolves a peer dependency through the removed one.
Removing a package from a workspace no longer forces a full re-resolution. The lockfile update drops the departed project's importer entry and prunes whatever only it depended on. A project that is still linked from a surviving project continues to be reported as an error #13696.
An install sharing a global virtual store no longer removes an incomplete package directory that another importer is still writing, which could fail with failed to remove existing directory ... prior to swap: Directory not empty. Such a directory is now repaired in place, and a package file left damaged by an interrupted install is restored instead of being kept.
pnpm sbom no longer emits components for optional platform-specific dependencies that cannot be installed on the current platform (for example, the native @rolldown/binding-* variants for other operating systems). Such packages are present in the lockfile but are never downloaded, so their license (and other metadata) could not be resolved and they appeared in the SBOM without one. pnpm sbom --lockfile-only still describes the whole lockfile graph, which is platform-independent by design.
An ssh:// git dependency pointing at a bracketed IPv6 host, such as ssh://[::1]/repo.git, is resolved now. Its colons were read as an SCP-style path separator, which turned the address into [:/1] and left the specifier unresolvable. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, an ssh:// git dependency written without user info — ssh://git.example.com/team/repo.git, git+ssh://git.example.com:2222/team/repo.git — no longer fails with TypeError: Cannot read properties of undefined (reading 'includes'). Only the user@host form worked before.
packageExtensions is now validated when the configuration is read, so a malformed entry (for instance a dependency range set to null) fails with an actionable error instead of crashing later during peer dependency resolution #13756.
Projects using resolutionMode: time-based now benefit from the fast lockfile update paths. A removal, a dependency group move, or a compatible range change no longer forces a full re-resolution just because the lockfile carries a time field #13696.
An install that drops the last dependent of a patched package no longer updates the lockfile in place and succeeds silently. Removing a dependency, widening ignoredOptionalDependencies, or adding a removal override could each prune the package while the patch stayed configured; such an install now falls back to a full resolution, which reports the unused patch with ERR_PNPM_UNUSED_PATCH. Under allowUnusedPatches, where the lockfile update is kept, the same install now warns that the patch went unused instead of saying nothing #13827.
Added interactive group selection to pnpm update --global --interactive.
Running pnpm setup, pnpm self-update, or a command that modifies the global installation (such as pnpm add --global) through sudo now prints a warning. pnpm keeps global packages and configuration in the invoking user's home directory, so running these commands as root silently operates on the root user's home directory instead of yours. They will fail with ERR_PNPM_SUDO_NOT_SUPPORTED in pnpm v12. Read-only global commands (such as pnpm bin --global) are unaffected.
Patch Changes
Fixed pnpm failing to start under asynchronous Node.js module loaders when no .pnpmfile.mjs exists pnpm/pnpm#11701.
Fixed minimumReleaseAge fallback for custom dist-tags so the selected version does not exceed the registry’s original tag target.
Removing a dependency from package.json and reinstalling no longer re-resolves the dependency graph. The importer's entry is dropped from pnpm-lock.yaml, anything it made unreachable is pruned, and a catalog entry that loses its last referent is removed — all without registry access. Installs still fall back to a full resolution when a package that stays resolves a peer dependency through the removed one, since that would change the surviving package's entry rather than only prune.
Changing a catalog entry to a different exact version no longer re-resolves the dependency graph. The package is replaced in pnpm-lock.yaml directly, reusing the same check the pnpm.overrides fast path applies: every locked dependency of the package must still satisfy the new version's manifest. Installs fall back to a full resolution when anything other than the catalog reaches the package — an importer that depends on it directly, or another package that depends on it — since the graph would then need both versions.
Fixed a CI regression where github:owner/repo dependencies (and other shorthand Git specifiers) would fail to install with Permission denied (publickey) on CI runners that lack SSH keys. The Git resolver no longer records an SSH URL unless the user explicitly wrote one (e.g. git+ssh:// or git@host:...):
The repository visibility probe (an HTTP HEAD request) now retries transient failures such as 429 Too Many Requests, so host throttling of CI runners is no longer mistaken for a private repository.
For non-SSH specifiers, anonymous HTTPS git ls-remote access is now tried before SSH, so a public repository whose visibility probe fails still resolves to a portable HTTPS URL instead of an SSH URL that only works where SSH keys are configured.
When every probe fails, the resolver falls back to HTTPS for shorthand and HTTPS-style specifiers, and only guesses SSH when the user explicitly provided an SSH URL.
A repository that could not be confirmed public is no longer resolved to the host's anonymous archive URL (e.g. codeload.github.com, which would fail to download for a private repository); it stays a regular git resolution so installs can use ambient Git credentials such as credential helpers and tokens.
Note that a private repository that is reachable both over authenticated HTTPS and over SSH now resolves to its HTTPS URL, where previous versions recorded the SSH URL.
ng build and nuxt build now work under the global virtual store: pnpm's built-in compatibility extensions add the tslib dependency that @angular/build uses without declaring and the unplugin dependency that @nuxt/vite-builder v4 uses without declaring.
Fixed link: dependencies under enableGlobalVirtualStore so linked children are materialized and slots remain isolated by their resolved link targets.
An install that skips resolution because pnpm-lock.yaml is already up to date now reacts fully to packages the lockfile removed — for example after pulling a lockfile in which a dependency was deleted. The hoist layer is recomputed, so a package that became hoistable when a direct dependency was removed is hoisted, and pendingBuilds entries for removed packages are dropped instead of staying pending forever.
The held-back-update warning printed by pnpm update no longer fires when minimumReleaseAge is the actual reason a newer version was not picked. The warning's baseline now applies the same maturity cutoff as the pick itself, so it no longer wrongly attributes the hold-back to "your manifests and already installed dependencies" or recommends an override that would defeat the age gate. See #13071.
Checking whether ignoredOptionalDependencies is up to date no longer reorders the configured patterns. The check sorted them in place, which could move an ! exclusion ahead of the pattern it excludes from and flip which optional dependencies were ignored.
Changing autoInstallPeers, dedupePeers, peersSuffixMaxLength, excludeLinksFromLockfile, or injectWorkspacePackages no longer re-resolves the dependency graph when the lockfile proves the setting cannot affect it: no package or project declares a peer dependency for the peer settings, and no project depends on a directory or on another workspace project for the link and injection settings. The new setting is recorded in pnpm-lock.yaml and the install proceeds from the existing resolution. Every other case still falls back to a full resolution.
Adding, editing, or removing an entry in patchedDependencies no longer re-resolves the dependency graph. Resolution never reads a patch — it only records the patch file's hash against the package it matches — so the install now rewrites the affected entries in pnpm-lock.yaml and materializes the patched package from the store instead. Installs still fall back to a full resolution when the patched package is reachable as a peer dependency, and when the new configuration would leave a patch unused while allowUnusedPatches is off, so ERR_PNPM_UNUSED_PATCH is still reported.
Resolving a private git repository no longer blocks on an interactive credential prompt: git ls-remote now fails fast with an authentication error when git has no credentials for the repository #13522.
Lockfile verification now honors offline mode by using cached registry metadata instead of reaching the registry. When the required metadata is not available locally, verification reports the same ERR_PNPM_NO_OFFLINE_META condition used by offline resolution.
POSIX shell shims now follow symbolic links before computing basedir, preventing execution failures when a shim is invoked via an external symlink on PATH#13405.
The automatic packageManager version switch works again on registries whose tarball URLs point at a different host than the registry itself (load-balanced feed proxies, Artifactory-style mirrors). Package-manager entries are now always recorded with integrity-only resolutions — the download URL is derived from the trusted bootstrap registry instead — and entries persisted in an invalid shape by an earlier pnpm are discarded and re-resolved instead of failing every command #13619.
Registries that serve no npm signature metadata (private mirrors and feed proxies commonly strip dist.signatures) no longer break the automatic packageManager version switch and pnpm self-update#13147. When the configured registry cannot provide a verifiable signature, pnpm now fetches the signature from registry.npmjs.org and verifies it against the same embedded npm keys over the installed integrity — which proves exactly the same thing. If no signature can be obtained from either source (for example, both are unreachable, or the registry publishes only a shasum), pnpm proceeds with a warning instead of failing, but only when the packages resolve through a registry configured in the user's own (non-project) configuration; the download stays pinned by the lockfile integrity, and a signature that exists but does not validate still fails the switch.
pnpm fetch, and any install run with virtualStoreOnly, no longer writes a .pnp.cjs loader under nodeLinker: pnp. These installs populate the virtual store without linking the project, so the loader would have claimed the project resolves out of a store it was never linked into. The importer links and node_modules/.package-map.json were already skipped; the PnP loader now follows the same rule.
Prevent pnpm from removing project files when modulesDir resolves to the project root.
Speed up installs after adding ignoredOptionalDependencies patterns by removing newly ignored optional dependencies and pruning packages that are no longer reachable without resolving the dependency graph again.
When a failed install re-copies a bin script from the store, rerunning pnpm install now reapplies the executable bit to the bin instead of leaving it non-executable #12742.
pnpm root -g and pnpm bin -g now print warnings to stderr instead of stdout, so their stdout stays a clean, machine-readable path. Previously, running either command with --global in a project that pins a package manager (e.g. via the packageManager field) printed a warning like [WARN] Using --global skips the package manager check for this project ahead of the path, breaking programs that capture the output as a path #13672.
In pnpm 12, pnpm root -g and pnpm prefix -g are now supported (they previously failed with ERR_PNPM_CLI_ROOT_GLOBAL_UNSUPPORTED / ERR_PNPM_CLI_PREFIX_GLOBAL_UNSUPPORTED), and the reporter output of dlx, create, config, sbom, with, store, prefix, root, and bin goes to stderr, matching pnpm 11.
pnpm setup no longer makes Node.js print a MODULE_TYPELESS_PACKAGE_JSON warning about dist/worker.js on every command. The package.json it writes next to a standalone executable now declares "type": "module".
pnpm update without saving no longer records a version that the manifest's range excludes. The kept range stays authoritative: a requested version outside it is skipped with a warning, and a requested range, a dist tag, or --latest resolves within it instead of past it. Previously each of these could write a lockfile entry that contradicted its own specifier, which the next pnpm install --frozen-lockfile rejected with ERR_PNPM_OUTDATED_LOCKFILE#12764.
pnpm version -r --json now outputs [] instead of human-readable text when no pending changes exist pnpm/pnpm#13217.
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.
If you use named registries
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:
npmjs: 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:
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.
Patch Changes
An empty http-proxy, https-proxy, proxy, or no-proxy value — from the .npmrc, pnpm-workspace.yaml, the CLI, or the HTTP_PROXY / HTTPS_PROXY / PROXY / NO_PROXY environment variables — no longer fails the install with ERR_PNPM_INVALID_PROXY. Empty settings read as unset, so a shell exporting HTTP_PROXY= disables the proxy, and an empty proxy= in the .npmrc no longer suppresses HTTPS_PROXY#13533.
proxy=false in the .npmrc or proxy: false in pnpm-workspace.yaml now turns proxying off instead of being read as a proxy host named false. false and null on https-proxy / http-proxy / no-proxy read as unset, and on the command line they are ordinary host names, since a flag carries its value verbatim.
The env lockfile no longer pins @pnpm/exe alongside pnpm when the wanted pnpm version is 12 or newer. From v12 the unscoped pnpm package is itself the native executable, so @pnpm/exe is not published for it and resolving it would fail. The engine identity check now verifies the native binary through whichever package ships it.
lexCompare and nerfDart are now published as @pnpm/text.ordinal-comparator and @pnpm/config.registry-auth-key. Use these instead of @pnpm/util.lex-comparator and @pnpm/config.nerf-dart.
Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
Dependency resolution is faster: package metadata is now filtered once per packument instead of once per dependency edge when minimumReleaseAge is active, and parsed semver versions and ranges are reused instead of re-parsed on every comparison.
Security: pnpm rebuild now refuses a lockfile whose packages key carries a path traversal in the package name (e.g. ../../../escaped@1.0.0), instead of running that package's lifecycle scripts and linking its bins in a directory outside the virtual store. Such a name is rejected with ERR_PNPM_INVALID_DEPENDENCY_NAME.
pnpm login no longer requires an interactive terminal when the registry supports web-based login: without a TTY it prints the authentication URL (skipping the QR code and the "Press ENTER to open the URL in your browser" prompt) and polls the registry until the browser approval completes. Only the classic username/password login still fails with ERR_PNPM_LOGIN_NON_INTERACTIVE in a non-interactive terminal.
The save-prefix setting now accepts =: newly added dependencies are saved with an explicit = operator (=1.2.3) instead of the setting being silently treated as the default ^.
Patch Changes
allowBuilds entries can now approve git-hosted packages that pnpm downloads as a tarball, such as github: dependencies (which are fetched from codeload.github.com rather than cloned), by their repository URL without the resolved commit hash. This matches the hashless git+ matching already supported for cloned git dependencies. For example:
This approves the package whether pnpm clones it or downloads a tarball, so the entry no longer has to be updated every time the pinned commit changes. GitLab and Bitbucket tarball downloads are matched the same way. Approving or denying a specific resolved commit by its full tarball dep path continues to work.
pnpm outdated --include-github-actions no longer blocks on an interactive git credential prompt when a workflow uses a private action repo.
Prevented minimumReleaseAge from replacing latest with a SemVer-greater version than the registry tag target #13034.
Fixed empty bundledDependencies and bundleDependencies arrays causing nondeterministic lockfile changes. See #13123.
The install summary no longer prints (X is available) when the registry's dist-tags.latest is still held back by the active minimumReleaseAge policy. The hint only ever names the actual latest tag, so an immature latest suppresses the hint instead of advertising the version pnpm just refused to install #11698.
pnpm update keeps the explicit = operator of an exact version pin: a dependency saved as =3.5.1 now updates to =3.5.2 instead of the bare 3.5.2. See #13168.
Preserve a workspace dependency's link: entry when a run does not target it — e.g. pnpm update <other-pkg> (with or without --recursive), or a plain install after a root/catalog dependency change — with injectWorkspacePackages, instead of spuriously rewriting it to a peer-suffixed file: protocol. See #10433.
Workspace dependencies declared with a relative path (e.g. "foo": "workspace:../foo") are no longer silently dropped from the workspace projects graph, so --filter selection and the topological order of recursive commands take them into account.
Fixed an installed optional dependency being left without one of its own required dependencies. When a package reached through optionalDependencies is installable on the current system but one of its regular dependencies is not, a lockfile-based install skipped that dependency and installed the parent anyway, so importing the parent failed with MODULE_NOT_FOUND. The dependency is now installed, and an install-check warning reports the incompatibility. A dependency is still only skipped when every path to it is optional, or when the package that pulls it in was itself skipped #13286.
pnpm setup now appends PNPM_HOME and the global bin directory to the GitHub Actions environment files (GITHUB_ENV and GITHUB_PATH), so later steps in the same job can run pnpm add --global and other global commands #9191.
Added support for publishConfig.name, which publishes a package under a different name than the one its manifest carries in the workspace. It is for a project whose published name is already taken by a sibling project, which otherwise has to be renamed by a build step just before publishing. Only the published artifact is renamed — dependents, pnpm-lock.yaml, and release tooling keep addressing the project by its manifest name — and the new name reaches the packed manifest, the tarball filename, and everything that addresses the package at the registry: the already-published check of pnpm publish -r, its registry selection, and the release-planning probes of pnpm change status and pnpm version -r#13345.
pnpm self-update no longer takes any instruction from the project it is run in:
pnpm is fetched through the same trusted registry and auth configuration used when switching pnpm versions, so a project .npmrc or pnpm-workspace.yaml can no longer redirect the download or attach credentials to it, and the project's default .pnpmfile.(c|m)js is no longer loaded. Pnpmfiles from trusted sources (the pnpmfile setting, the global pnpmfile, config dependencies) still apply.
The minimumReleaseAge settings in pnpm-workspace.yaml no longer affect self-update. They still govern the project's own dependencies; for self-update the cooldown now comes from the built-in default, your global config, a PNPM_CONFIG_* environment variable, or a command-line flag. This fixes self-update failing inside a workspace that raises the cutoff while succeeding everywhere else, and stops a repository from either waiving the cooldown or keeping you on an outdated pnpm by raising it.
The same applies to the trustPolicy settings and to ci: a project can no longer weaken the trust check that guards the pnpm download, nor re-enable the confirmation prompt that a CI run suppresses.
When self-update refuses a version that is younger than the cutoff, an interactive run now offers to update anyway; non-interactive runs still fail. CI never prompts, even on a runner that attaches a TTY.
Patch Changes
Fixed pnpm licenses list to report every version when the same package is installed under multiple aliases pnpm/pnpm#13438.
Sort pnpm dedupe --check snapshot changes for stable output across pnpm implementations.
Strip Unicode formatting characters from registry- and manifest-derived terminal output.
Speed up installs after compatible catalog or direct dependency range changes by retaining the locked version without resolving the dependency graph again.
Speed up installs after safe override changes by reusing unambiguous compatible dependency resolutions, pruning obsolete dependencies, applying independent replacements and removals together, and handling parent-scoped "-" overrides without full lockfile resolution.
Installing a local file: directory dependency with the global virtual store enabled no longer fails with TypeError: Cannot read properties of undefined (reading 'split')#13335.
Local directory dependencies — file: directories and injected workspace packages — now get a global-virtual-store slot of their own per project. They used to share one slot across every project that depended on a directory of the same name, so a project could end up linked to another project's copy of the dependency.
The Workspace column of pnpm update --interactive now falls back to the project's path when its name is only whitespace, as it already did for a missing or empty one — all three render an equally blank label otherwise.
Checking GitHub Actions dependencies for updates is now opt-in for every command. Neither pnpm outdated nor pnpm update reads the workflow files unless --include-github-actions is passed or update.githubActions is set to true in pnpm-workspace.yaml. Reading them runs git ls-remote against every referenced repository, which fails in environments where GitHub is not reachable the way pnpm assumes (a GitHub Enterprise Server, a custom certificate authority, or an offline network) #13254.
pnpm outdated accepts the --include-github-actions option too.
pnpm update --interactive now measures its table in terminal columns rather than in characters. A package name, workspace name, or version containing wide characters (CJK, most emoji) no longer knocks its row's columns out of line with the rest of the group, and a wide character in a version no longer aborts the command with Subject parameter value width cannot be greater than the container width#13357.
The Workspace column of pnpm update --interactive is more informative in two cases. A dependency outdated at the same version in several workspace projects is offered as one choice, since selecting it updates every project — that choice now names all of them instead of only the first. And a workspace project without a name is now labelled with its path rather than left blank, so several unnamed projects can be told apart.
An auto-installed optional peer is no longer hoisted at a version the workspace root's own dependency on that package excludes. resolvePeersFromWorkspaceRoot already made the workspace root's specifier decide which version a missing required peer is installed at; the optional-peer picker ignored it and always took the highest version present anywhere in the graph. In a workspace whose root pins postcss: 8.5.10, an importer that depends on webpack and declares no postcss of its own got postcss@8.5.22 hoisted for terser-webpack-plugin's optional postcss peer, leaving two postcss@8.5.x instances in the graph #13320.
overrides now also govern peers that pnpm auto-installs. Previously an override only rewrote dependencies declared in a manifest, so a peer nobody declares — installed because autoInstallPeers is on — resolved against its declared peer range and could bring in a second copy of the very package the override pinned. For example, with overrides: { react: npm:react@19.2.0 } and a lone lucide-react dependency, pnpm installed react@18.3.1; it now installs the pinned react@19.2.0#13320.
Under resolvePeersFromWorkspaceRoot, a workspace root dependency declared with link: or file: (or the path form of workspace:, such as workspace:../pkg) now satisfies another project's missing peer dependency at the linked package's own version, instead of being hoisted as a path. Those specifiers are relative to the project that declares them, so the same specifier reached a different directory — or none — from the project the peer was hoisted into, leaving a broken link. The root now has the same authority over the peer as it has when it declares the package with a version range #13373.
Installs through a pnpr server now apply the project's whole verification policy. minimumReleaseAgeExclude, minimumReleaseAgeIgnoreMissingTime, trustPolicy, trustPolicyExclude, trustPolicyIgnoreAfter, and trustLockfile were ignored, so excluded packages were still held back and a lockfile containing them could be rejected.
trustPolicy: no-downgrade no longer fails with TRUST_POLICY_INCOMPATIBLE_WITH_PNPR when a pnpr server is configured.
--frozen-lockfile and --no-prefer-frozen-lockfile are now honored on the pnpr path, instead of resolving and rewriting the lockfile anyway. Since frozenLockfile defaults to true on CI, a CI install through a pnpr server now fails on an out-of-date lockfile rather than updating it.
Workspace installs through a pnpr server no longer crash with Cannot read properties of undefined (reading 'filter') after linking, when minimumReleaseAge is active #13275.
Fixed pnpm dedupe updating valid catalog resolutions when another matching version exists in the lockfile.
pnpm -r run "/pattern/" --no-bail no longer exits zero when one of a project's matched scripts fails and a later one passes. The run summary carries a single status per project, and the passing script overwrote the recorded failure.
Restored the store block a first install prints, naming how packages were materialized and where the stores live #13315:
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: ~/.local/share/pnpm/store/v11
Virtual store is at: node_modules/.pnpm
The root project's pnpm:devPreinstall script now runs before resolution and linking, as it does in pnpm 11. It is skipped under --ignore-scripts, --lockfile-only and --dry-run, by pnpm fetch and pnpm rebuild, and by a repeat install that is already up to date. Workspaces that use the hook to prepare state the install depends on — such as next.js, which generates a placeholder next bin with it — were left with dependents linked against files that were never created #13313.
Prevented pnpm dedupe --check from removing an incompatible node_modules directory.
pnpm update --workspace no longer links dependencies the user never named:
Running it with updateConfig.ignoreDependencies configured no longer fails with ERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUND for a dependency that is only published to the registry. Such dependencies keep their specifiers, as they already did when no dependencies were ignored.
Passing package selectors that match no direct dependency no longer falls back to linking every workspace dependency.
Added a new setting, update.githubActionsServer, for specifying the base URL of the GitHub server that hosts the repositories of the GitHub Actions referenced by the workflow files (for example, a GitHub Enterprise Server). When the setting is not defined, the URL is read from the GITHUB_SERVER_URL environment variable, falling back to https://github.com. The URL must use the https:// or http:// protocol #13220.
pnpm outdated and pnpm update no longer fail when the refs of a GitHub Action's repository cannot be read (for example, when the action's repository is private or hosted on a different GitHub server). Such actions are now skipped with a warning.
Setting update.githubActions to false now makes pnpm outdated and the interactive pnpm update skip GitHub Actions dependencies.
Patch Changes
The token poll for web-based authentication no longer reads the body of non-OK or still-pending (HTTP 202) responses, and caps the token response body it does read at 64 KiB, so a malicious or compromised registry cannot exhaust memory through the poll pnpm/pnpm#12721.
Fixed catalog: references in dependencies and overrides failing to resolve when installing through a pnpr server, which errored with "No catalog entry '' was found for catalog 'default'." even though the catalog entry existed. Also fixed a crash on Windows when installing a nested workspace member (e.g. packages/foo) through a pnpr server #13232.
Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug #13164.
Revert script ordering change for pnpm run --sequential /regex/
Support the from-git argument in the pnpm version command.
When the authentication URL cannot be rendered as a QR code (for example when it exceeds the maximum QR data capacity), web-based login now displays the URL alone with a warning instead of aborting authentication pnpm/pnpm#12721.
The first release of a package now publishes the version written in its manifest verbatim, instead of bumping off it. pnpm version -r and pnpm change status check the registry for each release's current version; when that version is not yet published, the package debuts at it and its pending changesets apply only from the next release. A newly added package seeded at 1100.0.0 with a minor changeset is therefore published as 1100.0.0 rather than skipping straight to 1100.1.0.
Added a --changeset flag to pnpm update. Set update.changeset to true in pnpm-workspace.yaml to enable this behavior by default, and use --no-changeset to override the setting for one update. After the update completes, pnpm writes a .changeset/pnpm-update-<suffix>.md file declaring a patch bump for every workspace package whose dependencies or optionalDependencies were changed by the update and a major bump when peerDependencies changed, including packages that consume an updated catalog entry via the catalog: protocol. Private packages, packages without a name, and packages listed in the ignore array of .changeset/config.json are skipped. If .changeset/config.json does not exist, a warning is printed and no changeset is generated.
Added GitHub Actions dependencies to pnpm outdated and interactive pnpm update. Non-interactive updates can include them with --include-github-actions or by setting update.githubActions to true in pnpm-workspace.yaml. Updated actions are pinned to exact commit hashes with their release tags preserved in comments.
Added update and audit settings sections to pnpm-workspace.yaml, superseding the awkwardly named updateConfig, auditConfig, and top-level auditLevel settings:
update:ignoreDeps:# was updateConfig.ignoreDependencies- webpack- "@babel/*"audit:level:high# was auditLevelignore:# was auditConfig.ignoreGhsas- GHSA-xxxx-yyyy-zzzz
update.ignoreDeps lists dependency name patterns that pnpm update and pnpm outdated should skip. audit.level and audit.ignore tune pnpm audit.
The deprecated updateConfig, auditConfig, and auditLevel settings keep working until the next major version. When both a new section value and its deprecated counterpart are set, the new section takes precedence and a warning is printed. Both the TypeScript CLI and the Rust config surface (pacquet) recognize the new sections.
Patch Changes
Fixed pnpm add --save-exact/--save-prefix and pnpm update writing a package's version with the peerDependencies range's prefix (e.g. ^19.2.7 instead of the requested 19.2.7) whenever the same package also appeared in peerDependencies. A real dependencies/devDependencies/optionalDependencies entry now takes precedence over a same-named peerDependencies entry when computing the current specifiers #13108.
pnpm install now detects a supportedArchitectures change and re-evaluates previously skipped platform-specific optional dependencies, instead of reporting the project as up to date and leaving the packages for the old architecture set in place.
pnpm setup now removes leftover v10-layout shims at the top of PNPM_HOME, so pnpm self-update no longer warns about a v10 installation layout after PATH has been migrated to the v11 PNPM_HOME/bin layout. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, self-update also no longer treats a dangling legacy shim (one whose install target was garbage-collected) as a real v10 layout, so the warning can no longer fire on dead shim files.
Completed pnpm runtime installation parity for Node.js, Deno, and Bun, including runtime failure policy, target architecture selection, and dependency runtime engines. Runtime failure overrides now preserve explicit runtime dependencies without matching engine entries.
Fixed pnpm install running out of memory while resolving large dependency graphs #8441. The resolver kept full registry documents — per-version readmes, scripts, descriptions, and other install-irrelevant bulk — in memory for every package fetched with full metadata (optional dependencies, and packages re-fetched for minimumReleaseAge's publish timestamps). Every retained document is now condensed down to the field set installation actually reads, which reduces peak resolution memory by several times on workspaces with more than a thousand packages.
When a dependency's build script fails under enableGlobalVirtualStore, the global virtual store directory it was being built in is now removed for scoped packages too. Previously the cleanup resolved one directory level short of the hash directory for a scoped name, leaving a half-built directory behind that later installs would reuse.
Fixed pnpm login, pnpm adduser, and pnpm logout against a registry hosted under a URL subpath (e.g. https://example.com/npm/registry) when the configured URL has no trailing slash. Such URLs were left unnormalized, so the last path segment was dropped when building the login and token endpoints and the auth token was stored under a truncated key. Registry URLs with a path now always get a trailing slash appended during normalization, matching how root-level registry URLs are handled.
Optional peer dependencies declared only via peerDependenciesMeta (for example debug's supports-color peer) are now resolved from a satisfying version already present in the dependency graph, the same way explicitly declared optional peer dependencies are. Previously such peers were only resolved this way when the package's metadata was read back from the lockfile, so an unrelated dependency change could rewrite peer resolutions across the whole lockfile.
Patch Changes
Updated adm-zip to prevent crafted ZIP archives from causing excessive memory allocation.
pnpm version -r no longer writes a versioning-ledger entry with no consumed intents as a bare intents: key, which the next run failed to read with ERR_PNPM_INVALID_VERSIONING_LEDGER. Empty intent lists are now written as intents: [], and the ledger reader accepts the bare form left by earlier releases.
Fixed pnpr workspace resolution to preserve project names and versions for workspace: dependencies.
peerDependencies now accept dependency specifiers that carry a scheme — a named-registry spec (<registry>:<version>), an npm: alias, or a file:/git/URL spec — instead of rejecting them with ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION#13095. Such a peer is matched against the semver range carried by the specifier (work:5.x.x is checked as 5.x.x, npm:bar@^5 as ^5), or against * when it carries no version, while the original specifier still selects the package to auto-install. Bare name@version values, which are almost always a mistake, are still rejected.
Added pnpm doctor, which diagnoses the pnpm installation and the environment it runs in: the versions and install method, whether the global bin directory is on PATH, whether the store and cache are writable, which link strategies (reflink, hardlink, symlink) the store's filesystem supports, registry connectivity, and an offline file: install that exercises the resolve/store/link path end to end. Each check reports how to fix what it finds, and the command exits non-zero when any check fails.
Use --offline to skip the checks that need network access, --json for machine-readable output, and --benchmark to time the filesystem and install checks.
Added support for executing multiple scripts matching a RegExp passed to pnpm run (e.g., pnpm run "/^build:.*/"), running matched scripts in deterministic lexicographical order. Restored the --sequential (-s) CLI option for pnpm run, which forces workspaceConcurrency to 1 so that matched scripts run sequentially one by one across and within packages.
Patch Changes
Fixed pnpm install failing with ERR_PNPM_LOCKFILE_IS_SYMLINK when pnpm-lock.yaml is a symlink, as build sandboxes such as Bazel and Nix stage it #13073. Reading a lockfile through a symlink is allowed again, and an install that leaves the lockfile unchanged no longer rewrites it, so --frozen-lockfile no longer needs to write at all. Writing a changed lockfile through a symlink is still refused, as that would redirect the write onto the symlink's target.
Fixed frozen installs incorrectly treating equivalent Git dependency specifiers as a stale lockfile. See #13039.
pnpm owner ls now reports authentication and authorization failures (401/403) as dedicated errors that include the registry's response body, matching pnpm owner add/rm, instead of a generic Failed to fetch owners message.
Recover from a metadata cache entry that disappears (concurrent cache cleanup, antivirus) after the registry has already answered the conditional request with 304 Not Modified. The metadata is re-requested once without cache validators instead of failing the install with ERR_PNPM_CACHE_MISSING_AFTER_304.
A project pinned to a broken pnpm release via packageManager or devEngines.packageManager now reports which release is broken and what to do about it, instead of failing inside the installer. pnpm self-update already refused these releases; the version switch does too.
Prevent broken-lockfile errors from including snippets of the lockfile's contents.
pnpm self-update now checks that the version it installed can run before making it the active pnpm. A release that installs but cannot execute is discarded with an error instead of replacing a working installation.
Fixed an out-of-memory regression when workspace projects concurrently resolve a package with large registry metadata pnpm/pnpm#13077.
Fixed pnpm update rewriting exact version pins that use the = operator (for example =3.5.1) to a caret range (^3.5.1). Exact pins are now preserved and written back as the bare version. See #12745.
Fixed pnpm pack applying workspace-root ignore rules when a workspace package has its own .npmignore file.
Keep the interactive minimumReleaseAge approval prompt visible during pnpm install. The progress reporter now pauses its redraws while a prompt is waiting for input instead of overwriting it, so the install no longer hangs on a question the user cannot see #13019.
Fixed pnpm self-update failing to link native platform binaries stored in sibling global virtual store slots.
Added versioning.epics to pnpm-workspace.yaml. An epic ties a group of member packages to a lead package, constraining every member's major version to a band derived from the lead's major: while the lead is on major M, members live in M*100 … M*100+99. Members move independently inside the band (patch, minor, and a major intent that stays in-band); a bump that would carry a member past the band ceiling is rejected until the lead advances its own major. When a release plan takes the lead to a new stable major, every member re-bases to the band floor in the same plan. Membership is matched with pnpm's package selectors — name globs, ./-prefixed directory globs, and !-prefixed negations.
Added the team command for managing organization teams and team memberships on the registry, with create, destroy, add, rm, and ls subcommands and support for --otp, --parseable, and --json flags.
Added native workspace release management #12952: the new pnpm change command records change intents as changesets-compatible .changeset/*.md files (pnpm change status shows the pending release plan), and the bare pnpm version -r consumes them — bumping versions across the workspace with dependent propagation through workspace: ranges, fixed groups, a maxBump cap, --filter narrowing, and --dry-run — writing changelogs, and recording consumed intents in a committed ledger that keeps cherry-picks and merge-backs between release branches safe. Packages can be moved onto per-package release lanes with the new pnpm lane <name> --filter <pkg> command and back with pnpm lane main --filter <pkg> (pnpm lane shows the membership), releasing X.Y.Z-lane.N prereleases from the same runs that release stable versions of the packages on the main lane. Configuration lives under the new versioning key of pnpm-workspace.yaml (fixed, ignore, maxBump, lanes, changelog). When two workspace projects publish the same name, intent files, versioning.lanes, and versioning.fixed/ignore may reference a project by its workspace-relative directory path (e.g. "./pnpm/npm/pnpm") — the one additive extension to the changesets format, applied automatically by pnpm change.
Release changelogs default to registry storage (versioning.changelog.storage): no CHANGELOG.md is committed. Each release's section is composed at publish time and packed into the published tarball on top of the previously published version's changelog, and the consumed change intents are garbage-collected by a later pnpm version -r only once the registry confirms the version is published with its section. Set versioning.changelog.storage: repository to keep committed CHANGELOG.md files instead.
Added a new override selector form with an empty range — "pkg@": "<version>" — called a convergence override. It rewrites a dependency edge only when its exact version satisfies the edge's declared range, so compatible consumers converge on one version while incompatible consumers keep their own resolution — now and for any dependent added in the future #12794.
overrides:"form-data@": 4.0.6
The value must be an exact version. When a full resolution detects that every declared range also admits a newer version, pnpm warns that the override is stale and names the version to converge on. Previously an empty range in an override selector was undocumented and behaved like a bare (unscoped) override.
Patch Changes
A tokenHelper set in the global pnpm auth.ini is no longer rejected as project-level configuration. The guard that blocks tokenHelper from a project .npmrc only treated ~/.npmrc as a trusted source, so a helper written to auth.ini (for example by pnpm config set) failed on every command and could not even be removed with pnpm config delete. A tokenHelper in a workspace or project .npmrc is still rejected.
pnpm cache delete now removes a package's metadata from every metadata cache directory (metadata, metadata-full, and metadata-full-filtered), instead of only the one the current resolution mode reads. Previously a package cached under a different mode (e.g. metadata-full-filtered) was left behind. Closes #12753.
Fixed an injected workspace dependency (injectWorkspacePackages: true) incorrectly staying as file: instead of deduping back to link: when an unrelated, ordinary shared dependency resolved to a peer-suffixed variant for the target project's own copy but not for the injected occurrence. See #10433.
pnpm deploy now supports workspaces that use catalogs.
Fixed pnpm deploy with a shared lockfile so local file: tarball dependencies keep their package name in the generated deploy lockfile. This prevents warm-store deploys from failing with ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE when the tarball filename includes the version.
Options that follow create, exec, or test appearing as a subcommand of another command are now parsed instead of being silently treated as positional parameters. For example, pnpm team create @org:team --registry <url> previously ignored the --registry option and sent the request to the default registry.
pnpm add -g, pnpm update -g, pnpm setup, and the self-updater no longer fail with ERR_PNPM_MISSING_TIME when trustPolicy: no-downgrade or resolutionMode: time-based is set in the global config #12883. The decision to fetch full registry metadata now lives in one place, and the no-downgrade trust policy always requests full metadata (matching the self-updater), since the trust evidence it checks is missing from abbreviated metadata even on registries that include the time field.
pnpm list and pnpm why no longer crash with EMFILE: too many open files when a project has a large number of unsaved dependencies (packages present in node_modules but not in the lockfile). The reads of those packages are now concurrency-limited.
The published pnpm package no longer declares dependencies or devDependencies. Because the CLI bundles its runtime dependencies into dist/node_modules, those fields are dropped when packing, so npm install of the tarball no longer tries to resolve internal-only packages such as @pnpm/test-ipc-server. Closes #12955.
Fixed pnpm publish --otp and pnpm publish --batch --otp to send the configured OTP to the registry.
pnpm publish again sends the package's README to the registry as metadata, so registries can render it on the package page. The readme is always included in the published metadata (matching the npm CLI), while the embed-readme setting continues to control only whether the readme is written into the package.json inside the tarball. This restores the behavior that was lost when publishing became fully native. Closes #12966.
Fixed the dependency status check wrongly reporting "up to date" when a package.json, .pnpmfile.cjs, or patch file was edited in the same second as the previous install, on filesystems that record mtimes at whole-second resolution (for example ext4 with 128-byte inodes). The optimistic repeat-install fast path and verify-deps-before-run compared mtimes strictly, so a same-second edit whose mtime rounded down looked unchanged and re-resolution was skipped. Such a file's whole second is now treated as possibly-modified, falling through to the content check; behavior on sub-second filesystems is unchanged.
Retry package metadata requests when a registry or proxy returns 304 Not Modified to an unconditional request, preventing false ERR_PNPM_CACHE_MISSING_AFTER_304 failures pnpm/pnpm#12882.
If the retry also returns 304, report ERR_PNPM_META_NOT_MODIFIED_WITHOUT_CACHE instead.
Fixed pnpm update removing transitive lockfile entries when dedupePeerDependents is disabled and the selected package is absent pnpm/pnpm#12456.
Limit modern deploy lockfiles and localized virtual stores to dependencies reachable from the selected dependency groups.
A tokenHelper command is now given a 60-second time limit. A helper that hangs (deadlock, stuck I/O) is killed and reported as an error instead of leaving the command waiting forever.
Fixed orphaned child processes on Windows when pnpm exits on an error while commands spawned by pnpm exec or pnpm dlx are still running (for example, when one project's command fails during pnpm --recursive exec). The PIDs of these commands are now recorded when they are spawned and their whole process trees are terminated with taskkill on an error exit. Previously the cleanup relied on enumerating the system process list, which is so slow on Windows that the enumeration hit its timeout and the cleanup was silently skipped #12406.
pnpm pack now respects workspace-root .npmignore and .gitignore files when packing workspace packages.
a897ef7: Custom fetchers exported from a pnpmfile can now delegate by returning a { delegate: <resolution> } envelope: pnpm rewrites the package's resolution to the delegated shape and runs the built-in fetcher on it. This is the portable delegation form that also works in pacquet, where cafs and fetchers cannot be passed to the hook. Related to pnpm/pnpm#11685.
Patch Changes
2b02764: The changed-packages filter (--filter "...[<since>]") no longer allows an option-like <since> value (such as --output=<path>) to be interpreted as a git option — git now rejects it as a bad revision. The repository root is also resolved to the nearest .git entry, so the filter works in a git worktree checked out inside another repository's tree.
43711ce: pnpm outdated no longer checks the registry for dependencies that are resolved from local link:, file:, or workspace: references in the lockfile #12827.
3c6718b: Fixed a deadlock in peer dependency resolution: pnpm install hung forever when a peer dependency cycle spanned a project's own dependencies and auto-installed peer providers, for example when installing electron-builder@26.15.3#12921.
252f15e: Fixed peer dependency auto-install picking a version the peer range rejects. In a workspace with several projects, a package declaring a peer dependency with a semver range (for example ^1.0.0) could get the highest version found anywhere in the workspace (for example a 2.0.0 resolved for another project) instead of a version that satisfies the range. Peers are now deduplicated onto the highest preferred version that satisfies the declared range, and when none does, the range is resolved from the registry.
Also fixed re-resolving with an existing lockfile hoisting a different peer version than a fresh install of the same manifest: root dependencies reused from the lockfile were invisible to peer hoisting, so a peer that a root dependency provides could be bound to another version.
a38adda: pnpm self-update <version> now installs the requested pnpm version when it matches the currently running version but is missing from the global self-update directory.
6a85968: pnpm stage list now stops paginating after a fail-safe cap of 1000 pages, so a misbehaving registry cannot keep the command looping forever.
eee7c9a: verify-deps-before-run no longer spawns a pnpm install when pnpm is executed in a directory that has no package.json. A mistyped command run outside a project (for example pnpm witch 10 login) used to crash with a confusing error from the spawned install; now it fails with the regular "no package.json found" error.
508b8c2: Added the pnpm access command for managing package access and visibility on the registry, supporting listing packages and collaborators, getting and setting package status and MFA requirements, and granting or revoking team access.
Patch Changes
c70e33e: Allow allowBuilds entries for git-hosted packages to match by repository URL without pinning the resolved commit hash. This lets trusted git repositories keep running their build scripts after branch updates without approving each new commit, while package-name-only rules still do not approve git-hosted artifacts.
3067e4f: Reduced peak memory usage during cold-cache dependency resolution. The metadata fetch is memoized for the whole resolution phase, and it was retaining each package's raw registry response body (used only to mirror the response to disk) for that entire time. The memoized cache now holds a body-less copy, so the raw body only lives as long as the call that writes the disk mirror. On large graphs that fetch full metadata (e.g. with minimumReleaseAge or trustPolicy enabled) this cuts peak RSS by roughly 30%, back in line with pnpm 10. The resolved lockfile is unchanged.
51300fd: Prevent a crafted pnpm-lock.yaml from writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g. ../../../tmp/x@1.0.0) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshot version: "../../x") is now rejected at formatGlobalVirtualStorePath, the single point every global-virtual-store slot path funnels through — closing the same escape in the isolated linker, the resolver's dependency-graph builder, and the config-dependency installer.
f8058eb: Reject symlinked pnpm-lock.yaml files when reading or writing the env lockfile document.
9318a11: Allow registries and namedRegistries to be configured in the global config.yaml file.
51300fd: Fixed a path traversal vulnerability where a dependency whose manifest name was a scoped path traversal (e.g. @x/../../../<path>) could be written outside node_modules to an attacker-controlled location during pnpm install, even with --ignore-scripts. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.
14332f0: Fail instead of silently removing an optional dependency's locked entries from pnpm-lock.yaml when the registry cannot resolve it. Previously, when registry metadata lacked a version that the lockfile already pinned (for example, a mirror that had not synced a recent release yet), pnpm install and pnpm dedupe silently dropped the optional dependency's entries — emptying maps such as the platform binaries of @napi-rs/canvas — so the lockfile differed between machines and frozen installs on other hosts had nothing to link #12853.
fecfe83: Fixed peer dependency resolution with autoInstallPeers when a workspace package depends on a version of a package that a transitive dependency's self-contained closure also provides for itself. The peer providers that are attached to the root project for reuse are no longer peer-resolved a second time in the root context, so packages inside such a closure no longer get their peers bound to the root project's incompatible version #4993.
5a4daec: ${...} environment-variable placeholders in the httpProxy, httpsProxy, noProxy, proxy, and noproxy settings are no longer expanded when these settings come from a project's pnpm-workspace.yaml. They now receive the same protection already applied to registry, namedRegistries, and pnprServer.
d1da02e: pnpm publish no longer prints credentials when the target registry is configured with inline user:pass@ credentials (e.g. registry=https://user:pass@example.com/). They are now redacted both from the "publishing to registry" line and from the OIDC (trusted publishing) failure messages.
dcfc611: pnpm self-update now honors trustPolicy=no-downgrade. It resolves the target pnpm version against full registry metadata, so it refuses to switch to a version whose supply-chain trust evidence is weaker than an earlier-published one, the same way a regular install does.
a8ad82d: Register the pn alias in generated shell completion scripts.
25bd5c3: Fixed standalone installer downgrades from pnpm v12 to v11.
23996e9: pnpm runtime set <name> <version> now validates its arguments: the name must be node, deno, or bun, and the version must not contain a comma. Previously these were interpolated straight into a pnpm add selector, where an unsupported name or a comma (e.g. node 22,is-positive) could be misread as a list of packages or a local directory and install unintended packages or bins.
e2e3c81: Added the issues command as an alias of bugs, so pnpm issues opens the package's bug tracker URL in the browser.
8491f8e: Added the prefix command which prints the current package prefix directory (or global prefix directory if -g / --global is used).
3425e80: Added an _auth setting for configuring registry authentication as a single structured (URL-keyed) value. It can be set in the global pnpm config (config.yaml) or, for CI, via the pnpm_config__auth environment variable. The env form sidesteps the GitHub Actions / bash / zsh limitation that broke the existing pnpm_config_//host/:_authToken=… form (env var names containing /, :, or . are silently dropped). Closes #12314.
The value is keyed by registry URL so each secret is explicitly bound to the host that may receive it. Registry URL keys must use http or https and must not include credentials, query strings, or fragments:
Within each registry URL, @ means registry-wide/default credentials and package scopes like @org bind credentials to that scope on the same host. The only supported credential field is authToken (maps to _authToken / bearer auth); the deprecated basicAuth / username + password forms are intentionally not accepted here.
Each entry also infers a trusted registry route: @ routes the default registry (and pnpm add <pkg> resolves there), and @org routes that scope. Because the credential and destination host arrive in one trusted value, repo-controlled pnpm-workspace.yaml or project .npmrc cannot redirect the token to a different host. _auth is honored only from the env var and the global config — it is ignored in a project pnpm-workspace.yaml / .npmrc, so repo-controlled config can never supply registry auth. Precedence: CLI flags (--registry, --@scope:registry) > pnpm_config__auth > global config.yaml_auth > pnpm-workspace.yaml.
Both pnpm_config__auth (lowercase, documented form) and PNPM_CONFIG__AUTH (all-caps, the shell convention some CI runners apply) are honored. If both are set, lowercase wins unless it is empty, in which case uppercase is used. The env var wins over the global config.yaml_auth on a conflicting key. tokenHelper is not supported in _auth. Parsing is strict: a malformed value (bad JSON, wrong shape, invalid registry URL or scope, an unsupported credential field) fails fast with an error rather than being silently dropped.
Pacquet parity note: the pacquet (Rust) port supports the same single credential field as the TS CLI: authToken.
a33eeec: pnpm self-update and packageManager version-switching can now install and link pnpm v12 (the Rust port), published with equal content under both the pnpm and @pnpm/exe names on the next-12 dist-tag. Its native binaries ship as @pnpm/exe.<platform>-<arch> packages, which pnpm's built-in installer links directly — no Node.js launcher, so the command pays no Node startup cost. v12 is initialized exactly like @pnpm/exe, including per-platform global-virtual-store hashing. From v12 onward the install converges on the unscoped pnpm package (the Rust exe) — even when updating from the SEA @pnpm/exe build.
1dd12bd: When resolving through a pnpr install-accelerator server, pnpm no longer forwards its own upstream registry credentials in the resolve request. Only the Authorization header identifying the caller to pnpr is sent. The pnpr server now selects upstream credentials from its own route policy (operator-configured upstream credential aliases), so private dependencies resolve through a pnpr-managed alias the caller is authorized to use, rather than by sending the client's registry tokens to the server.
1e81761: Expose web authentication authUrl and doneUrl in JSON error output when OTP is required in a non-interactive terminal #12724.
Patch Changes
2f389d6: Added the Node.js release team's new signing key (Stewart X Addison, 655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD) to the embedded Node.js release keys, so runtimes whose SHASUMS256.txt is signed by the new releaser verify successfully.
acbdb94: Fixed shell tab completion not suggesting workspaces after the -F alias for --filter option.
dcabb78: Fixed pnpm up -r <pkg> bumping unrelated packages that have open semver ranges. Previously, any update mutation nullified the lockfile-derived preferredVersions globally, so packages with ^x.y.z ranges could re-resolve to newer compatible versions even though the user only asked to update a specific package. The install layer now always seeds preferredVersions from the lockfile, and caller-supplied preferred versions (such as the vulnerability penalties of pnpm audit --fix) layer on top of the seed instead of replacing it. The targeted package still bumps: the per-resolve updateRequested flag makes the resolver ignore the target's own lockfile pins.
On Windows, removing or updating a global package now also cleans up the node.exe flavor of a bin, so a stale node.exe no longer survives on PATH after uninstall, and a new global install no longer silently overwrites an existing node.exe.
pnpm add -g pnpm@<version> (and @pnpm/exe@<version>) is now rejected like the bare pnpm form, pointing to pnpm self-update.
Dependency aliases read from a global package's manifest are validated before being joined onto node_modules paths, preventing a tampered manifest from escaping the install directory.
Each global install group is created in its own freshly-made directory (no longer reusing a colliding or pre-existing path).
Removing or updating a global package no longer unlinks a bin that belongs to a different globally installed package.
25c7388: pnpm now rejects jsr: specifiers whose package name is not a valid npm package name — an empty scope or name (e.g. jsr:@scope/), path separators inside the name, or any other shape validate-npm-package-name rejects — with ERR_PNPM_INVALID_JSR_PACKAGE_NAME instead of silently converting them into a malformed @jsr/... npm package name.
25c7388: pnpm now rejects named-registry specifiers (e.g. gh:) whose package name is not a valid npm package name — an empty scope (e.g. gh:@/bar), path separators inside the name (e.g. gh:@scope/../name), or any other shape validate-npm-package-name rejects — with ERR_PNPM_INVALID_NAMED_REGISTRY_PACKAGE_NAME instead of passing the name through to registry URLs and metadata cache file paths.
96da7c5: node-gyp's gyp_main.py and gyp entrypoints are now packed with the executable bit in the pnpm and @pnpm/exe tarballs. Without it, building native addons from source could fail with a permission error.
99982b9: Sped up resolution and reduced memory use against registries that ignore npm's abbreviated metadata format and always return the full package document (for example, Azure DevOps Artifacts). pnpm now strips such documents down to the abbreviated field set before caching them. Resolution output is unchanged, and registries that honor the abbreviated format (such as the npm registry) pay no extra cost.
11a7fdd: Sped up offline and --prefer-offline resolution on large workspaces (e.g. pnpm dedupe --offline, pnpm install --offline). Package metadata loaded from the local cache is now kept in memory, so each package's metadata is parsed once per command instead of once per dependent that references it.
2c7369d: pnpm pack-app now rejects --entry / pnpm.app.entry and --output-dir / pnpm.app.outputDir values that are absolute paths or escape the project directory via .. (or a symlink that resolves outside it), and refuses to write the produced executable when its target path already exists as a symlink (or other non-regular file). This prevents a repository-controlled package.json from embedding host files (such as an SSH key) into the produced executable, writing build artifacts outside the project, or overwriting an arbitrary file through a committed symlink. The new error codes are ERR_PNPM_PACK_APP_ENTRY_OUTSIDE_PROJECT, ERR_PNPM_PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT, and ERR_PNPM_PACK_APP_OUTPUT_FILE_NOT_REGULAR.
When ad-hoc signing macOS targets, pnpm pack-app now runs the system codesign by absolute path and resolves ldid to a location outside the project, so a repository-controlled node_modules/.bin on PATH cannot hijack the signer.
ce5d5a5: Relative paths in patchedDependencies are now resolved against the lockfile directory when computing patch file hashes, so running pnpm install from a subdirectory no longer fails with ENOENT looking for the patch file in the wrong location #12762.
ebb4096: pnpm peers no longer reports a conflict for a missing peer dependency that is ignored via pnpm.peerDependencyRules.ignoreMissing.
dcabb78: Fixed a prototype-pollution hazard when seeding preferred versions: a dependency named __proto__ in a manifest or in pnpm-lock.yaml could write through Object.prototype (or crash the install) while the preferred-versions map was being built. The maps are now null-prototype objects, so crafted package names land as plain keys.
f38e696: Hardened pnpm deploy --force so it refuses unsafe deploy targets such as workspace roots, parent directories, out-of-workspace paths, and symlinked target parents.
806c3ec: pnpm no longer warns about ignored project-level auth settings when PNPM_CONFIG_NPMRC_AUTH_FILE points at the project .npmrc — setting it to that file is an explicit opt-in to trusting it, so auth env variables in it are expanded pnpm/pnpm#12480.
991405e: Restore differential rendering (ansi-diff) to fix duplicated output lines introduced by #12351.
c121235: Fixed the topological order of --filtered commands (pnpm run, pnpm exec, pnpm publish, pnpm pack, pnpm rebuild) when the selected projects depend on each other only transitively through projects that were not selected. Previously such selected projects could run concurrently or in the wrong order; now a project always runs after the selected projects it transitively depends on, while projects without a real dependency relationship still run concurrently. This now also holds for prod-only filters (--filter-prod), which resolve order through the production dependency graph so transitive production dependencies are respected without pulling back the dev dependencies the filter drops, and for selections that mix --filter with --filter-prod#8335.
d539172: pnpm pack and pnpm publish no longer follow a symlinked workspace LICENSE file when injecting it into a package that has no license of its own. Following the symlink could pack bytes from outside the workspace into the published tarball.
dcabb78: Fixed pnpm up <pkg> producing a different result than a fresh install of the same manifests would. The resolver now distinguishes updateRequested (true only for packages that match the user's update target) from the broader update flag, and for the targeted package ignores only its own lockfile-derived preferred-version pins — so the target re-resolves exactly as if its lockfile entries were deleted and pnpm install ran. Preferred versions a fresh install applies (manifest pins, versions propagated down the dependency chain, and the vulnerability-avoidance penalties of pnpm audit --fix) stay in effect, so an update never installs duplicate versions that a reinstall from scratch would not reproduce. When a preferred version holds the update target below the newest version its range admits, pnpm now prints a warning explaining that reaching the newer version everywhere requires an override.
dcabb78: pnpm update <dep>@<version> now prints a warning when <dep> is only present as a transitive dependency: the requested version cannot be applied there (updates resolve the target the way a fresh install would), and the warning recommends adding the version to pnpm.overrides instead, which is the mechanism that does pin transitive dependencies. Closes #12744.
a6c4d5f: When a dependency cannot be found in the registry (404) or the registry has no matching version, and a workspace project with the same name exists only at non-matching versions, the error now reports the available workspace versions (ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE) instead of the raw registry failure pnpm/pnpm#1379. Other registry failures (authorization, network, server errors) still propagate unchanged. The pacquet (Rust) resolver applies the same behavior.
bae694f: Some registries generate tarballs on-demand and cannot provide an integrity checksum in their package metadata. In that case pnpm now computes the integrity from the downloaded tarball and stores it in the lockfile, so the entry is verifiable on subsequent installs instead of being written without an integrity (which would fail the next install). This also applies to --lockfile-only: the tarball is downloaded so its integrity can be computed. A lockfile entry that is still missing its integrity is rejected as a ERR_PNPM_MISSING_TARBALL_INTEGRITY lockfile verification violation (the install fails closed) rather than being silently re-fetched.
6c35a43: Added --exclude-peers to pnpm sbom. With auto-install-peers (the default), peer dependencies resolve into the lockfile and are otherwise indistinguishable from the package's own dependencies. The flag drops peer dependencies (and any transitive subtree reachable only through them) from the SBOM. CycloneDX 1.7 has no scope or relationship that expresses "consumer-provided peer", so omission is the only spec-clean handling. The flag name matches pnpm list --exclude-peers; note the SBOM flag prunes a peer's exclusive subtree, which is stricter than pnpm list (which only hides leaf peers).
Patch Changes
25a829e: pnpm audit --fix now writes a single combined minimumReleaseAgeExclude entry per package (e.g. axios@0.18.1 || 0.21.1) instead of one entry per version, matching the format documented for the setting. Existing per-version entries in pnpm-workspace.yaml are merged into the combined form rather than left as duplicates. Installs that auto-collect immature versions into minimumReleaseAgeExclude now report the same combined entries, so the "Added N entries" message matches what is written to the manifest #12534.
1cbb5f2: Fixed non-deterministic peer resolution that could add or remove an optional transitive peer — for example @babel/core, reached through styled-jsx — from a package's peer-dependency suffix across otherwise identical installs, churning the lockfile and causing intermittent pnpm dedupe --check failures in CI. When a package's children are resolved by one occurrence (the "owner") and reused by a deeper consumer, whether that consumer inherited the owner's missing peers depended on whether the owner's resolution had finished yet — a race under concurrent resolution. The decision is now a function of the dependency graph's structure rather than resolution-completion order.
d577eea: Fixed a Windows flakiness in pnpm dlx where a failed install could surface a spurious EBUSY: resource busy or locked error. The cleanup of a partially-populated dlx cache is now best-effort with retries and no longer masks the original error.
ec7cf70: Shortened the pnpm dlx cache path so deep dependency trees no longer overflow Windows' MAX_PATH, which could make a dependency's lifecycle script fail with spawn cmd.exe ENOENT.
05b95ab: Fixed pnpm hanging (and crashing with an unhandled promise rejection) when a non-retryable network error such as SELF_SIGNED_CERT_IN_CHAIN occurs while fetching from a registry. The error is now rejected through the returned promise instead of being thrown inside the detached retry callback.
d3f68e2: Fix a pnpm audit performance regression on lockfiles that contain dependency cycles. The reachable-vulnerability pruning added in pnpm 11.5.1 only memoized acyclic subtrees, so any node whose subtree touched a cycle — together with all of its ancestors — was recomputed on every query, making the path walk quadratic. Reachability is now computed once per node using Tarjan's strongly-connected-components algorithm, so cyclic graphs are handled in linear time #12212.
The audit path walk also no longer recurses, so a deeply nested dependency graph can no longer overflow the call stack, and the install path to each finding is tracked without per-node copying, keeping memory linear in the graph depth.
322f88f: Fix failed optional dependency updates so they don't rewrite unrelated dependency specs #11267.
1488db1: When enableGlobalVirtualStore is toggled on for a project that was previously installed without it, stale hoisted symlinks under node_modules/.pnpm/node_modules are now replaced instead of being left pointing at the old per-project virtual store location #9739.
6545793: Fixed pnpm install --ignore-workspace overwriting the allowBuilds map in pnpm-workspace.yaml. The ignored builds of a package with a build script were auto-populated into allowBuilds even though --ignore-workspace was passed, clobbering committed true/false values with the set this to true or false placeholder #12469.
fbdc0eb: Fixed minimumReleaseAgeExclude and trustPolicyExclude so multiple exact-version entries for the same package behave the same as a single || disjunction entry. Previously only the first matching rule's versions were honored, so a config like [form-data@4.0.6, form-data@2.5.6] could still flag form-data@2.5.6 as violating minimumReleaseAge, while [form-data@4.0.6 || 2.5.6] worked as expected #12463.
fa7004b: The in-memory package metadata cache is now populated on the exact-version disk fast path, so repeated resolutions of the same package within one install no longer re-read and re-parse the on-disk metadata. In large monorepos this brings the time for adding a new package down from minutes to seconds. The in-memory cache key now also includes the registry, so a package of the same name served by two different registries in a single install can no longer share a cache slot and resolve the wrong tarball.
0a154b1: Fixed pnpm patch dropping the package name (and leaking internal option fields) when the patched dependency resolves to a single git-hosted version.
4d3fe4b: The pnpr resolver endpoints moved under the reserved /-/pnpr namespace: POST /v1/resolve is now POST /-/pnpr/v0/resolve and POST /v1/verify-lockfile is now POST /-/pnpr/v0/verify-lockfile. The capability handshake at GET /-/pnpr advertises protocol version 0 to match. This keeps every pnpr-proprietary route in npm's reserved namespace, so it can never collide with a package path.
0ec878d: Removing a runtime dependency now removes the matching devEngines.runtime or engines.runtime entry that was materialized from it. Blank runtime selectors are normalized to latest.
17e7f2c: pnpm sbom now emits a CycloneDX issue-tracker external reference for components (and the root) whose package.json declares a bugs URL. Email-only bugs entries are skipped, since the reference requires a URL.
a84d2a1: Add @pnpm/resolving.tarball-url, which builds and recognizes the canonical npm tarball URL of a package. It vendors getNpmTarballUrl (previously the external get-npm-tarball-url package) and adds isCanonicalRegistryTarballUrl, the predicate the lockfile writer uses to decide whether a tarball URL is derivable from name+version+registry (and can therefore be omitted from pnpm-lock.yaml).
Exposing isCanonicalRegistryTarballUrl lets a custom resolver (pnpmfile resolvers) fronting a proxy that serves tarballs on a non-canonical path (e.g. an ephemeral localhost:<port>) rewrite the resolved tarball to the canonical form, so nothing host-specific is persisted to the lockfile. Previously this logic was private to @pnpm/lockfile.utils.
Two correctness fixes are included while consolidating the logic: the scoped-package unescape now handles uppercase %2F as well as %2f (percent-encoding is case-insensitive), and protocol-insensitive comparison strips only a leading http(s):// scheme instead of splitting on the first :// (which could truncate URLs containing a later ://).
852d537: Lockfile verification no longer reports a registry metadata fetch failure (for example a 403/401 on a private registry, or a network error) as ERR_PNPM_TARBALL_URL_MISMATCH. When the registry can't be reached to verify an entry, the install now aborts with the registry's own fetch error (such as ERR_PNPM_FETCH_403, which already explains the authentication situation) instead of mislabeling a transport failure as lockfile tampering. Registry fetch errors no longer leak basic-auth credentials embedded in the registry URL (https://user:pass@host/) into their message.
c112b61: Added a --dry-run option to pnpm install. It runs a full dependency resolution and reports what an install would change, but writes nothing to disk (no lockfile, no node_modules) and always exits with code 0. This mirrors the preview semantics of npm install --dry-run#7340.
179ebc4: pnpm run --no-bail now exits with a non-zero exit code when any of the executed scripts fail, while still running every matched script to completion. This makes the exit-code behavior of --no-bail consistent between recursive and non-recursive runs (recursive runs already failed at the end). Previously, a non-recursive pnpm run --no-bail always exited with code 0, even when a script failed #8013.
0474a9c: Added support for generating Node.js package maps at node_modules/.package-map.json during isolated and hoisted installs. Added the node-experimental-package-map setting to inject the generated map into pnpm-managed Node.js script environments, and the node-package-map-type setting to choose between standard and loose package maps.
dcededc: pnpm sbom now marks components reachable only through devDependencies with CycloneDX scope: "excluded" and the cdx:npm:package:development property. The excluded scope documents "component usage for test and other non-runtime purposes", which matches the semantics of a devDependency; the property is the CycloneDX npm-taxonomy marker emitted by @cyclonedx/cyclonedx-npm, so both modern (scope) and existing (property) consumers are covered. Components reachable at runtime (including installed optionalDependencies) omit scope and default to required.
1495cb0: Added per-package SBOM generation with --out and --split flags. Use --out out/%s.cdx.json to write one SBOM per workspace package to individual files, or --split for NDJSON output to stdout. When --filter selects a single package, the SBOM root component now uses that package's metadata. Workspace inter-dependencies (workspace: protocol) and their transitive dependencies are included. Author, repository, and license fall back to the root manifest when the package doesn't define them.
293921a: feat(view): support searching project manifest upward when package name is omitted
When running pnpm view without a package name, the command now searches
upward for the nearest project manifest (package.json, package.yaml, or package.json5) and uses its name field.
If the manifest exists but lacks a name field, an error is thrown.
This change also replaces the find-up dependency with empathic for
improved performance and consistency across workspace tools.
Patch Changes
29ab905: Fixed pnpm update overriding the version range policy of a named catalog whose name parses as a version (e.g. catalog:express4-21). The catalog: reference carries no pinning of its own, so the prefix from the catalog entry (such as ~) is now preserved instead of being widened to ^#10321.
bee4bf4: Security: validate config dependency names and versions from the env lockfile (pnpm-lock.yaml) before using them to build filesystem paths. A committed lockfile with a traversal-shaped configDependencies name (such as ../../PWNED) or version (such as ../../../PWNED) could previously cause pnpm install to create symlinks or write package files outside node_modules/.pnpm-config and the store. Names must now be valid npm package names and versions must be exact semver versions; the same validation is applied to optional subdependencies of config dependencies, and to the legacy workspace-manifest format before any lockfile is written. See GHSA-qrv3-253h-g69c.
96bdd57: Fix link: workspace protocol switching to file: after pnpm rm is run from inside a workspace package whose target workspace dependency has its own dependencies, when injectWorkspacePackages: true is set. Follow-up to #10575, which fixed the same symptom for workspace packages without dependencies.
302a2f7: No longer warn about using both packageManager and devEngines.packageManager when the two fields pin the same package manager at the same version with the same integrity hash (e.g. both pnpm@11.5.1+sha512.…). Previously the hash was stripped from the legacy packageManager field but not from devEngines.packageManager, so even identical specifications looked like a mismatch #12028.
The warning still fires on any genuine divergence, and several cases now state the specific reason instead of a single generic message: a different package manager, a different version, or contradictory integrity hashes for the same version.
3f0fb21: Fixed the progress line showing leftover characters from external processes that write to the terminal between progress updates (e.g. an SSH passphrase prompt would leave a fragment like added 0sa':). The interactive reporter now redraws each frame in place, erasing to the end of the display before reprinting, so any such remnants are cleared #12350.
564619f: Fixed pnpm approve-builds reporting "no packages awaiting approval" when a build-script dependency whose approval was revoked (e.g. after git stash drops the allowBuilds from pnpm-workspace.yaml) is re-added. The revoked packages are now correctly recorded in .modules.yaml so approve-builds can find them. #12221
3d1fd20: Skip the redundant "target bin directory already contains an exe called node" warning on Windows when the existing node.exe already matches the target (same hard link or identical content) pnpm/pnpm#12203.
1b02b47: Fix macOS Gatekeeper blocking native binaries (.node, .dylib, .so) by removing the com.apple.quarantine extended attribute after importing them from the store.
When pnpm imports files from its content-addressable store into node_modules, macOS preserves extended attributes, including com.apple.quarantine. If this xattr is present on a store blob (e.g. it was first written under a Gatekeeper-enabled app such as a Git client), it propagates to node_modules, and Gatekeeper blocks the native binary from loading even though pnpm already verified the file's integrity against the lockfile.
After importing a package, pnpm now strips com.apple.quarantine from its native binaries, matching Homebrew's behaviour of dropping quarantine from verified downloads. The cleanup is macOS-only, runs in a single batched xattr call per package, is restricted to native binaries (other files are untouched), and is non-fatal (it logs a warning on unexpected errors).
61969fb: Fix pnpm install with optimisticRepeatInstall incorrectly reporting Already up to date when pnpm-lock.yaml changed but project manifests did not. This affected workflows such as checking out or restoring only the lockfile #12100.
Also fixes checkDepsStatus to use the correct lockfile path when useGitBranchLockfile is enabled, so the optimistic fast-path and lockfile modification detection work with pnpm-lock.<branch>.yaml files instead of always stat'ing pnpm-lock.yaml. Merge-conflict detection now reads the resolved lockfile name as well, and with mergeGitBranchLockfiles enabled every pnpm-lock.*.yaml is scanned for modifications and conflicts. The git branch is now resolved by reading .git/HEAD directly (no process spawn) and uses the workspace directory rather than process.cwd().
5c12968: Fix recursive updates of transitive dependencies when the update command mixes transitive dependency patterns with direct dependency selectors. For example, pnpm up -r "@babel/core" uuid now updates matching transitive @babel/core dependencies even when uuid is a direct dependency selector #12103.
9d79ba1: Register the pnpm update --no-save flag in the CLI help and option parser.
0474a9c: Fixed pnpm import for Yarn v2 lockfiles when js-yaml v4 is installed.
9e0c375: Fixed pnpm install repeatedly prompting to remove and reinstall node_modules in a workspace package when enableGlobalVirtualStore is enabled. The post-install build step recorded a per-project node_modules/.pnpm virtual store directory in node_modules/.modules.yaml, overwriting the global <storeDir>/links value the install step had written. The next install then detected a virtual-store mismatch (ERR_PNPM_UNEXPECTED_VIRTUAL_STORE). The build step now derives the same global virtual store directory as the install step #12307.
223d060: Document the --cpu, --os and --libc flags in the output of pnpm install --help. These flags were already supported but were only documented on the website #12359.
e85aea2: Avoid reading README.md from disk when publishing if the publish manifest already provides a readme field. The README is now only read lazily, inside createExportableManifest, when it is actually needed.
3188ae7: Fixed pnpm peers check to accept loose peer dependency ranges such as >=3.16.0 || >=4.0.0- when the installed peer version satisfies the range #12149.
531f2a3: Fixed pnpm update rewriting a workspace: dependency that points at a local path (e.g. workspace:../packages/foo/dist) into a normalized link: or version-range specifier. Such specifiers are now preserved verbatim when the workspace protocol is preserved #3902.
fe66535: Fixed a lockfile non-convergence bug where an incremental install kept a duplicate transitive dependency that a fresh install would not produce. When a package is reused from the lockfile, its child edges are taken verbatim and bypass the preferred-versions walk, so a transitive dependency could stay pinned to an older version even after a direct dependency resolved to a higher version that satisfies the same range. The resolver now refreshes such a stale pin to the higher direct-dependency version during resolution — so the older version is never resolved or fetched, and the incremental result converges to the fresh one.
6d35338: pnpm install detects changes inside local file dependencies again. The optimistic repeat-install fast path only tracks manifest and lockfile modification times, so edits inside a local dependency's directory (or a repacked local tarball) were reported as "Already up to date". Projects with local file dependencies (file: and bare local path or tarball specifiers, declared directly or through pnpm.overrides) now always run a full install, which refetches those dependencies, matching pnpm v10 behavior #11795.
4ca9247: Preserve the existing Node.js runtime version prefix when resolving node@runtime:<range> to a concrete version.
30c7590: Create shorter CAFS temporary package directories to leave room for lifecycle scripts that create IPC socket paths under TMPDIR.
13815ad: Reporter output (warnings, progress) for pnpm store and pnpm config subcommands now goes to stderr instead of stdout. This fixes scripts that capture their stdout (e.g. PNPM_STORE=$(pnpm store path), pnpm config list --json | jq) from getting warnings mixed into the result.
1c05876: Avoid relinking unchanged child dependencies and remove stale child links during warm installs.
817f99d: Fixed lockfile churn where a package's transitivePeerDependencies could be dropped (and shift between packages) when the package participates in a dependency cycle. A cycle re-entry resolves against truncated children, so it must not be cached as "pure"; otherwise sibling occurrences of the same package short-circuit and lose transitive peers depending on traversal order #5108.
eba03e0: Fix pnpm install reporting "Already up to date" after a catalog entry in pnpm-workspace.yaml was reverted to a previous version. After an update modified a catalog, the workspace state cache stored the pre-update catalog versions, so reverting the entry back to its original version was not detected as an outdated state #12418.
3b54d79: pnpm update now keeps lockfile overrides that resolve through a catalog in sync with the catalog. Previously, when an override referenced a catalog (e.g. overrides: { foo: 'catalog:' }) and pnpm update bumped that catalog entry, the lockfile's catalogs advanced while the resolved overrides kept the old version. The resulting lockfile was internally inconsistent, so a later pnpm install --frozen-lockfile failed with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.
9d0a300: Fixed pnpm version --recursive so it honors the workspace selection. In recursive mode the version bump now applies to the packages resolved from the workspace filter (selectedProjectsGraph), matching the behavior of pnpm publish --recursive, instead of always bumping every workspace package #11348.
Added a new setting frozenStore (--frozen-store) that lets pnpm install run against a package store on a read-only filesystem (e.g. a Nix store, a read-only bind mount, an OCI layer). When enabled, pnpm opens the store's SQLite index.db through the immutable=1 URI — bypassing the WAL/-shm sidecar creation that otherwise fails on a read-only directory — and suppresses every store-write path (the index.db writer and the project-registry write). Pair it with --offline --frozen-lockfile against a fully-populated store. Under the global virtual store, package directories live inside the store, so if the store is missing the build output of a package whose lifecycle scripts are approved (or that has a patch), pnpm fails up front with ERR_PNPM_FROZEN_STORE_NEEDS_BUILD rather than crashing mid-build on a read-only write — seed the store with those builds first. Incompatible with --force and with a configured pnpr server, since both write into the store; the side-effects cache is likewise not written under frozenStore. If the store is missing its content directory, the install fails fast with ERR_PNPM_FROZEN_STORE_INCOMPLETE rather than attempting to initialize it. The read-only immutable=1 open requires Node.js >=22.15.0, >=23.11.0, or >=24.0.0; on older runtimes --frozen-store fails with a clear ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE error. Bin-linking also tolerates a read-only store: under the global virtual store a package's bin source lives inside the store, so the chmod that makes it executable would be refused — with EPERM/EACCES, or with EROFS on a genuinely read-only filesystem. That chmod is redundant when the seed already ships its bins executable with a normalized shebang, so it is now skipped in that case, while a non-executable bin (or one still carrying a Windows CRLF shebang) on a read-only store still errors.
When pacquet (the Rust port of pnpm) is declared in configDependencies, pnpm now delegates dependency resolution to it too — not just materialization — provided the installed pacquet is new enough to support full resolving installs (>= 0.11.7).
Previously pacquet only ran in frozen-install mode: pnpm always resolved the dependency graph itself (writing pnpm-lock.yaml) and handed pacquet a finished lockfile to fetch / import / link. With pacquet >= 0.11.7, a non-frozen pnpm install (default isolated nodeLinker, plain install) is delegated to pacquet end-to-end in a single pass — pacquet resolves the manifests, writes the lockfile, and materializes node_modules. pnpm detects the capability from the installed pacquet's version; older pacquet releases keep the resolve-then-materialize split, and add / update / remove still resolve in pnpm (it has to mutate the manifests first). This remains an opt-in preview of the Rust install engine #11723.
Added a new opt-in --batch flag to pnpm publish --recursive that sends all selected packages to the registry in a single PUT /-/pnpm/v1/publish request instead of one request per package. The target registry has to implement the batch publish endpoint (pnpr does); registries that don't are reported with a clear ERR_PNPM_BATCH_PUBLISH_UNSUPPORTED error. The batch is processed all-or-nothing by pnpr: if any package in the batch fails validation, none of the packages are published.
Patch Changes
Reject path-traversal and reserved dependency aliases (such as ../../../escape, .bin, .pnpm, or node_modules) that come from a lockfile rather than a freshly resolved manifest. A crafted lockfile alias could otherwise be joined directly under a hoisted node_modules directory, letting package files be written outside the intended install root or overwrite pnpm-owned layout.
The fix adds two layers:
The nodeLinker: hoisted graph builder now validates each alias at the directory sink (safeJoinModulesDir), matching the validation pnpm already performs when resolving aliases from manifests.
The lockfile verification gate (verifyLockfileResolutions) now runs an always-on, policy-independent check that rejects any importer or snapshot dependency alias that is not a valid package name, failing the install early — before any fetch or filesystem work — for every node linker at once.
Made shared package child resolution deterministic when the same package is reached through multiple contexts. pnpm now chooses the shallowest occurrence, then importer order, then parent path, instead of letting request timing decide the child context and missing-peer report pnpm/pnpm#12358.
Fix garbled summary line after submitting pnpm update -i and pnpm audit --fix -i. The interactive checkbox prompt previously printed every selected choice's full table row (label, current/target versions, workspace, URL) joined by commas, producing a wall of text after pressing Enter. The summary now lists only the selected package names (or vulnerability keys) by setting an explicit short per choice; the in-progress selection UI is unchanged.
Prevent pnpm patch-remove from removing files outside the configured patches directory.
Fixed pnpm publish ignoring strictSsl: false when publishing to registries with self-signed certificates. The strictSSL option is now forwarded to libnpmpublish / npm-registry-fetch so that strict-ssl=false in .npmrc or strictSsl: false in pnpm-workspace.yaml is respected during publish, the same way it is for pnpm installpnpm/pnpm#12012.
Fixed Cannot destructure property 'manifest' of 'manifestsByPath[rootDir]' as it is undefined regression introduced in 11.6.0 when running pnpm add <pkg> outside a workspace on Windows. selectProjectByDir was keying the resulting ProjectsGraph by opts.dir instead of project.rootDir, so downstream manifestsByPath lookups missed when the two paths normalized differently (typically drive-letter casing). pnpm/pnpm#12379
Git dependencies that point to a subdirectory of a repository (repo#commit&path:/sub/dir) keep their path in the lockfile again. Since the integrity of git-hosted tarballs started being pinned in the lockfile, any install that actually downloaded the tarball rebuilt the lockfile resolution as { integrity, tarball, gitHosted } and dropped the path field, while installs served from the store kept it — so the field disappeared seemingly at random. Without path, later installs from that lockfile silently unpacked the repository root instead of the subdirectory #12304.
Fixed nondeterministic lockfile output that made pnpm dedupe --check fail intermittently in CI. When a locked peer provider was pinned for a dependency that has no child dependencies of its own, the pinned provider leaked into the shared parent scope, so siblings resolved after it could pick up an optional peer they should not see. Which siblings were affected depended on resolution order, which varies with network timing.
Sped up pnpm install with a frozen lockfile by running lockfile verification (the policy revalidation gate added for minimumReleaseAge/trustPolicy and the tarball-URL anti-tamper check) concurrently with fetching and linking instead of blocking the whole install on it. Dependency lifecycle scripts are still held back until verification succeeds, so no script runs on an unverified lockfile: if verification fails the install aborts before any dependency build, and if linking finishes first the install waits for the verification verdict before completing.
User-defined npm_config_* environment variables are now preserved during lifecycle script execution. Previously, all npm_-prefixed env vars were stripped, which caused user-set variables like npm_config_platform_arch to be lost pnpm/pnpm#12399.
pnpm can now use different auth tokens for different package scopes, even when those scopes use the same registry URL.
Previously, auth was selected only by registry URL. If @org-a and @org-b both used https://npm.pkg.github.com/, they had to share the same token. This caused problems for registries that issue tokens per organization or per scope.
Configure a scope-specific token by adding the package scope after the registry URL in the auth key:
pnpm login --registry=https://npm.pkg.github.com --scope=@org-a writes the token to the same scope-specific auth key.
When installing or publishing @org-a/*, pnpm uses ORG_A_TOKEN. For @org-b/*, pnpm uses ORG_B_TOKEN. Packages without a matching scope continue to use the registry-wide fallback token.
pnpm setup no longer prompts to approve build scripts for @pnpm/exe when installing the standalone executable. pnpm links the platform-specific binary itself, so the package's install scripts are skipped during the global self-install #12377.
Close lockfile reads deterministically before rewriting lockfiles and keep pacquet's virtual store directory length aligned with pnpm on Windows.
A 304 Not Modified answer from the registry now renews the cached metadata file's mtime, so the minimumReleaseAge freshness shortcut keeps serving resolutions from the cache. Previously, once a cached packument grew older than minimumReleaseAge, every subsequent install re-validated it against the registry forever, because a 304 never rewrites the file.
Updated dependency ranges. Notably:
@pnpm/logger peer dependency range moved to ^1100.0.0.
msgpackr 1.11.8 → 2.0.4 (store index files remain byte-compatible in both directions).
Fixed a Windows-only hang where a failed command could take 20–46 seconds to exit. On error, pnpm enumerates descendant processes (via pidtree) to terminate them, which on Windows shells out to wmic/PowerShell Get-CimInstance Win32_Process — a lookup that is extremely slow on some machines. The lookup is now bounded by a short timeout so it can no longer stall the process exit.
⚠️ Security fix — environment variables in a project .npmrc (action may be required)
Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands ${ENV_VAR} placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
Environment variables are still expanded in trusted config: your user-level ~/.npmrc, the global config, CLI options, and environment config.
If your authentication broke after upgrading, move the token out of the committed .npmrc:
# Writes to your user/global config, not the repository:
pnpm config set"//registry.npmjs.org/:_authToken""$NPM_TOKEN"
Or keep the ${NPM_TOKEN} line but put it in your user-level ~/.npmrc instead of the repo. In GitHub Actions, actions/setup-node with registry-url already writes a user-level .npmrc, so NODE_AUTH_TOKEN keeps working. For other CI where editing each pipeline is hard, set PNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc (or NPM_CONFIG_USERCONFIG=.npmrc) in the CI environment to declare the project .npmrc trusted.
pnpm install completes without re-resolving when pnpm-lock.yaml was deleted but node_modules is intact: the up-to-date check now treats the current lockfile (node_modules/.pnpm/lock.yaml) — the record of what the previous install materialized — as the wanted lockfile, verifies the manifests still match it, restores pnpm-lock.yaml from it, and reports "Already up to date". Previously this scenario triggered a full resolution and a re-verification of every locked package against the registry.
615c669: Added support for configuring URL-scoped registry settings through npm_config_//… and pnpm_config_//… environment variables, for example:
This provides a file-free way to supply registry authentication. Because the registry a value applies to is encoded in the (trusted) environment variable name, it is host-scoped by construction and cannot be redirected to another registry by repository-controlled config. The environment value is treated as trusted config: it takes precedence over a project/workspace .npmrc but is still overridden by command-line options. When the same key is provided through both prefixes, pnpm_config_ wins.
Raised the default network concurrency from min(64, max(cpuCores * 3, 16)) to min(96, max(cpuCores * 3, 64)). Package downloads are I/O-bound, not CPU-bound, so deriving the floor from the core count left machines with few cores (for example 4-vCPU CI runners) downloading only 16 tarballs at a time and unable to saturate a low-latency registry. The networkConcurrency setting still overrides the default.
Patch Changes
Improved the warning printed when a project .npmrc uses an environment variable in a registry/proxy URL or in registry credentials. The message now explains why the setting was ignored and how to migrate it to a trusted source — for example by moving the line to the user-level ~/.npmrc or running pnpm config set "<key>" <value> — with a link to https://pnpm.io/npmrc. The pnpm config set example is only suggested when the key has no ${...} placeholder, so the snippet is always safe to copy-paste.
Print a "Lockfile passes supply-chain policies (verified 2h ago)" message when lockfile verification is skipped because a cached verdict for the same lockfile content and policy is reused. Previously the cached short-circuit was completely silent, which made it look like the policy gate never ran #12324.
Platform-specific optional dependencies are now skipped even when their os/cpu/libc fields are missing from the registry metadata or the lockfile. Some registries strip these fields from the package metadata, which made pnpm download and install the binaries of every platform regardless of supportedArchitectures. The missing platform fields of an optional dependency are now inferred from its name (e.g. @nx/nx-win32-arm64-msvc → os: win32, cpu: arm64), so foreign-platform binaries are skipped without even downloading them #11702.
⚠️ Security fix — environment variables in a project .npmrc (action may be required)
Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands ${ENV_VAR} placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
Environment variables are still expanded in trusted config: your user-level ~/.npmrc, the global config, CLI options, and environment config.
If your authentication broke after upgrading, move the token out of the committed .npmrc:
# Writes to your user/global config, not the repository:
pnpm config set"//registry.npmjs.org/:_authToken""$NPM_TOKEN"
Or keep the ${NPM_TOKEN} line but put it in your user-level ~/.npmrc instead of the repo. In GitHub Actions, actions/setup-node with registry-url already writes a user-level .npmrc, so NODE_AUTH_TOKEN keeps working. For other CI where editing each pipeline is hard, set PNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc (or NPM_CONFIG_USERCONFIG=.npmrc) in the CI environment to declare the project .npmrc trusted.
Stopped expanding environment variables in repository-controlled registry/proxy request destinations and registry credential values from .npmrc, and in workspace registry URLs from pnpm-workspace.yaml. Move dynamic registry URL and token configuration to trusted user, global, CLI, or environment config.
Resolve package-manager bootstrap dependencies with trusted user or CLI registry and network config, and reject package-manager env-lockfile records that do not use registry package paths with integrity-only resolutions before auto-switch execution.
Avoid writing packageManagerDependencies to pnpm-lock.yaml when package manager policy is set to onFail: ignore or pmOnFail: ignore#12228.
Avoid running dependency-status auto-install when the dependency status is unavailable without a project manifest.
Using the $ version reference syntax in overrides (e.g. "react": "$react") now prints a deprecation warning. The syntax still works, but catalogs are the recommended way to keep an overridden version in sync with the rest of the workspace. Reference a catalog entry with the catalog: protocol instead.
Fixed pnpm config get globalconfig to return the global config.yaml path again pnpm/pnpm#11962.
Fixed bare --color so it does not consume the following CLI flag, allowing command shorthands like --parallel to expand correctly and forms like pnpm --color with current <command> to dispatch the inner command instead of failing with MISSING_WITH_CURRENT_CMD.
Fix pnpm install ignoring enableGlobalVirtualStore toggle by including it in the workspace state settings check #12142.
Security: pnpm now verifies the npm registry signature of a package-manager binary before spawning it, so a cloned repository cannot make pnpm download and execute an arbitrary native binary.
This covers two paths that select an executable from repository-controlled input:
pacquet install engine — declaring pacquet (or @pnpm/pacquet) in configDependencies opts in to pnpm's Rust install engine. pnpm now verifies that the installed pacquet shim and the host's @pacquet/<platform>-<arch> binary carry a valid npm registry signature for their exact name@version, and refuses to run pacquet (failing the command) if the signature does not verify or cannot be checked. The only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.
automatic version switch / self-update — the packageManager / devEngines.packageManager field makes pnpm download and run a specific pnpm version. pnpm now verifies the registry signature of pnpm, @pnpm/exe, and the host platform binary before installing/spawning them, and refuses to run an engine whose signature does not match a published, signed release. The check runs only on an actual download (store cache miss), so it does not add a network round trip to every command.
In both cases the signature is verified over the installed integrity, against npm's public signing keys that ship embedded in the pnpm CLI (like corepack), so bytes substituted via a tampered lockfile or a repository-controlled registry fail verification — and a registry the user did not vouch for cannot supply its own signing keys. The signed packument is fetched from the configured registry, so an npm mirror works transparently. Verification fails closed: if it cannot be completed (for example, the registry is unreachable), the command fails rather than running an unverified binary. The embedded keys are kept current by a release-time check against npm's signing-keys endpoint.
Made peer-dependent deduplication deterministic. When a peer-suffixed package variant was a subset of two or more mutually incompatible larger variants, the variant it collapsed into depended on the order importers were resolved in, which varies between machines. This could resolve the same workspace to different lockfiles on different platforms and make pnpm dedupe --check alternate between passing and failing.
Reject invalid package names and versions from staged tarball manifests before deriving filenames for pnpm stage download.
Clarified in CLI help that the pnpm store is trusted shared state and store integrity checks are corruption detection, not a tamper boundary for untrusted store writers.
Reject reserved manifest bin names ("", ".", "..", and scoped forms such as @scope/..) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted.
Require trusted package identity before package-name allowBuilds entries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as the allowBuilds key. Lockfile verification now rejects lockfiles where a registry-style dependency path (name@semver) is backed by a git, directory, or git-hosted tarball resolution (ERR_PNPM_RESOLUTION_SHAPE_MISMATCH), so the dependency path is a reliable artifact identity by the time scripts can run.
Security: pnpm now verifies the OpenPGP signature of a downloaded Node.js runtime's SHASUMS256.txt before trusting its integrity hashes.
When a repository requests a Node.js runtime (e.g. via devEngines.runtime / useNodeVersion), the download mirror is repository-configurable through node-mirror:<channel>. The integrity of the downloaded binary was only checked against SHASUMS256.txt fetched from that same mirror — a circular check that a malicious mirror could satisfy by serving a tampered binary together with a matching SHASUMS256.txt. pnpm then executes the binary (for example to run lifecycle scripts).
pnpm now fetches SHASUMS256.txt.sig and verifies the detached OpenPGP signature against the Node.js release team's public keys, which ship embedded in the pnpm CLI. A mirror that serves a tampered binary cannot also produce a valid signature, so the download fails to verify. The embedded keys are kept current by a release-time check against the canonical nodejs/release-keys list.
The musl variants from the hardcoded unofficial-builds.nodejs.org mirror are not repository-configurable and are signed by a different key, so they continue to be trusted over TLS.
Peer dependency resolution now reuses the peer contexts already recorded in the lockfile when those providers are still present in the dependency graph and still satisfy the peer ranges. This avoids unnecessary peer-context rewrites during lockfile regeneration. Current manifest choices remain authoritative: a newly added, explicitly updated, or aliased direct provider, a changed nested provider, or a locked version that no longer satisfies the range still takes precedence.
The lockfile verifier now checks that a registry entry pinning an explicit tarball URL points at the artifact the registry's own metadata lists for that name@version. Previously a tampered lockfile could pair a trusted name@version with an attacker-chosen tarball URL (and a matching integrity for those bytes), so the install fetched the attacker's bytes. A mismatch — or any entry that can't be confirmed against the registry — is rejected with ERR_PNPM_TARBALL_URL_MISMATCH. Non-registry resolutions (file:, git-hosted, etc.) and registry entries without an explicit tarball URL (the URL is reconstructed from name+version+registry, so it is inherently bound) are unaffected; non-standard registry tarball URLs (npm Enterprise, GitHub Packages) still pass because they match the metadata.
Fix pnpm update --recursive --lockfile-only <pkg>@<version> crashing with Invalid Version when the catalog entry for <pkg> is a version range (e.g. ^21.2.10) and catalogMode is strict or prefer. The catalog–version comparison now skips the equality check when either side is a range rather than passing a range to semver.eq(), so range specifiers fall through to the existing mismatch handling instead of throwing #11570.
Avoided a Node.js crash when pnpm exits after network requests on Windows.
Fixed packages being materialized into the virtual store without their root-level files (package.json, LICENSE, README, root entrypoints) when multiple pnpm install processes ran against the same store/workspace concurrently. The fast import path used to destructively empty the shared target directory, so a concurrent importer could wipe files another importer had already written; if the surviving files included the package.json completion marker, every later install treated the broken directory as complete and never repaired it. The fast path now imports directly only when it can create the target directory exclusively, and otherwise builds the package in a private temp directory and atomically renames it into place #12197.
Fix dependency build scripts not running under the global virtual store (enableGlobalVirtualStore).
In a workspace install, dependency build scripts are deferred to a single rebuild pass (buildProjects). That pass resolved each package's location from the classic node_modules/.pnpm/<depPathToFilename> layout, which does not exist under the global virtual store — so native dependencies (e.g. packages using node-gyp / prebuild-install) were never built and failed to load at runtime (Cannot find module .../build/Release/*.node).
buildProjects now resolves the global-virtual-store projection directory (<storeDir>/links/<hash>, computed with the same graph hash the installer uses) when enableGlobalVirtualStore is set, and serializes concurrent builds of the same shared projection so parallel workspace projects don't race on the same directory.
Don't promote a runtime: dependency (such as the Node.js version from devEngines.runtime or pnpm runtime set) into a catalog when catalogMode is strict or prefer. A runtime: dependency round-trips to devEngines.runtime, which only recognizes the runtime: protocol; cataloging it rewrote the manifest entry to catalog:, which broke that round-trip, stranded it in devDependencies, and left devEngines.runtime untouched.
Skip lockfile minimumReleaseAge/trustPolicy verification for non-registry tarball protocols (for example file:), so local tarball dependencies are not incorrectly checked against npm registry metadata.
Improve pnpm audit performance by pruning non-vulnerable lockfile subtrees and stopping path enumeration once vulnerable findings reach the path cap.
Avoid crashing when the workspace state cache is partially written or malformed.
Set npm_config_user_agent for root lifecycle scripts during headless installs.
Preserve the integrity field of a remote (non-registry) tarball dependency when its lockfile entry is rebuilt. Re-resolving such a dependency without re-fetching it (for example via pnpm update, or when another dependency changes) produced a resolution with no integrity — URL/tarball resolvers only learn the integrity after the tarball is downloaded — so the previously recorded integrity was dropped, making later installs fail with ERR_PNPM_MISSING_TARBALL_INTEGRITY#12067.
Normalize a string repository field into the { type, url } object form when creating the publish manifest, matching npm's behavior. Some registries (e.g. Gitea/Codeberg) reject a string repository with a 500 Internal Server Error during pnpm publish#12099.
Preserve compatible optional peer versions already present in the lockfile when resolving dependencies.
Fixed inconsistent resolution of a peer dependency that is shared through a diamond. When a package peer-depends on both another package and one of that package's own peer dependencies (for example @typescript-eslint/eslint-plugin peer-depends on both @typescript-eslint/parser and typescript, and @typescript-eslint/parser peer-depends on typescript), pnpm no longer reuses a hoisted instance of the shared peer that was resolved against a different version #12079.
Added a new hoistingLimits setting for nodeLinker: hoisted installs, mirroring yarn's nmHoistingLimits. It accepts none (the default — hoist as far as possible), workspaces (hoist only as far as each workspace package), or dependencies (hoist only up to each workspace package's direct dependencies). Originally proposed in #6468, closing #6457.
Replaced enquirer with @inquirer/prompts for all interactive prompts. Fixes the update -i scrolling overflow bug where long choice lists were clipped in the terminal #6643.
User-facing changes:
pnpm update -i / pnpm update -i --latest: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via usePagination
pnpm audit --fix -i: Same scrolling fix for vulnerability selection
pnpm run / pnpm exec (with verifyDepsBeforeRun=prompt): Confirmation prompt updated
Vim-style j/k keys still work for up/down navigation in all interactive prompts.
Internal: The OtpEnquirer and LoginEnquirer DI interfaces changed from { prompt } to { input } / { input, password } respectively. Plugins or custom builds that inject their own enquirer mock will need to update.
Staged publishes are now recognized in the trust scale. When a package version's registry metadata carries an approver field, it is treated as the strongest trust evidence (ranked above trusted publishers and provenance attestations), since staged publishes require 2FA publish approvals. This prevents false-positive trust downgrade errors when moving from a staged publish to a lower trust level #11887.
Patch Changes
Fix pnpm hanging during peer resolution when an aliased install pulls in transitive packages with mutual peer cycles at different depths in the dependency tree (for example, pnpm i nuxt@npm:nuxt-nightly@5x). Cycles whose members hit the findHit cache instead of running their own calculateDepPath are now short-circuited by sibling resolutions at the level where the cycle is detected, so the cached path promises no longer deadlock. #11999.
Fix pnpm dist-tag add and pnpm dist-tag rm against npmjs.org failing without --otp with [ERR_PNPM_UNAUTHORIZED] You must be logged in to set dist-tag … "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.". pnpm now sends npm-auth-type: web on dist-tag writes and surfaces the resulting OTP challenge through the existing browser-based 2FA flow (the same withOtpHandling helper used by pnpm publish), so the browser opens, the user authenticates, and the dist-tag is set on retry. --otp=<code> continues to work via the classic flow.
Fix minimumReleaseAgeExclude handling in npm resolution fast paths so excluded packages do not get pinned to stale versions. Excludes are honored consistently during publishedBy metadata selection and cache-mtime shortcuts.
Fix the integrity field being dropped from the lockfile entry of a remote (non-registry) https-tarball dependency when an unrelated package is installed afterwards. URL/tarball resolvers do not return an integrity (it is only known after the tarball is downloaded), so when such a dependency was reused from the lockfile without being re-fetched, its integrity was lost. It is now carried over from the existing resolution. With pnpm's lockfile-integrity hardening, the missing integrity made subsequent --frozen-lockfile installs fail with ERR_PNPM_MISSING_TARBALL_INTEGRITY. #12001.
Skip dependency re-resolution when pnpm-lock.yaml is missing but node_modules/.pnpm/lock.yaml exists and still satisfies the manifest. pnpm install now reuses the materialized snapshot to regenerate pnpm-lock.yaml instead of walking the registry to rebuild it from scratch, turning the cache+node_modules variation into a near-no-op for users who deleted the lockfile but kept the install #11993.
--frozen-lockfile still refuses to proceed when pnpm-lock.yaml is absent — the regenerated lockfile must be committed, so failing loudly is the correct behavior for CI.
Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, pnpm install (non-frozen) would log ERR_PNPM_TARBALL_INTEGRITY, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.
pnpm install now exits with ERR_PNPM_TARBALL_INTEGRITY and a hint pointing at the new opt-in flag.
The only opt-in is pnpm install --update-checksums — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.
--force and pnpm update deliberately do not bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. --frozen-lockfile behavior is unchanged. --fix-lockfile keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass.
pnpm runtime set <name> <version> now saves the runtime to devEngines.runtime by default instead of engines.runtime. Pass --save-prod (or -P) to save it to engines.runtime instead #11948.
Patch Changes
Fix a credential disclosure issue where an unscoped _authToken (or _auth, or username + _password, or tokenHelper) defined in one source — ~/.npmrc, ~/.config/pnpm/auth.ini, a workspace .npmrc, CLI flags, etc. — would be sent as an Authorization header to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (cert, key) so they aren't presented to a registry their author didn't choose.
pnpm now rewrites each unscoped per-registry setting (_authToken, _auth, username, _password, tokenHelper, cert, key) to its URL-scoped form at load time, using the registry= value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding registry= therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. ca/cafile are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally.
Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since npm@9, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g. //registry.example.com/:_authToken=... or //registry.example.com/:cert=...).
@pnpm/network.auth-header: removed the defaultRegistry parameter from createGetAuthHeaderByURI and getAuthHeadersFromCreds. Now that credentials are URL-scoped at load time, the merged configByUri never contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed.
Fix pnpm deploy crashing with ENOENT: ... lstat '<deployDir>/node_modules' when configDependencies declares pacquet (pacquet or @pnpm/pacquet). The deploy directory never installs config dependencies, so the install engine they designate isn't on disk to invoke; the nested install now skips them.
Reject git resolutions whose commit field is not a 40-character hexadecimal SHA before invoking git. A malicious lockfile could otherwise smuggle a value such as --upload-pack=<command> through git fetch / git checkout, which on SSH or local-file transports executes the supplied command.
Limit concurrent project manifest reads while listing large workspaces to avoid EMFILE errors.
Reject patch files whose diff --git headers reference paths outside the patched package directory. Previously a malicious .patch file added via a pull request could write, delete, or rename arbitrary files reachable by the user running pnpm install.
Improve the log message that pnpm prints after auto-adding entries to minimumReleaseAgeExclude when minimumReleaseAge is set without minimumReleaseAgeStrict. The message previously referred to the internal "loose mode" terminology, which wasn't searchable in the docs; it now tells the user to set minimumReleaseAgeStrict to true if they want these updates gated behind a prompt instead #11747.
Reject dependency aliases that contain path-traversal segments (such as @x/../../../../../.git/hooks) when reading them from a package manifest or symlinking them into node_modules. A malicious registry package could otherwise use a transitive dependency key to make pnpm install create symlinks at attacker-chosen paths outside the intended node_modules directory.
Reject pnpm-lock.yaml entries whose remote tarball resolution: block is missing the integrity field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips integrity:) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under --frozen-lockfile. pnpm now fails closed at lockfile-read time with ERR_PNPM_MISSING_TARBALL_INTEGRITY. Git-hosted tarballs (gitHosted: true or a URL on codeload.github.com / bitbucket.org / gitlab.com) and file: tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.
Validate devEngines.runtime and engines.runtime version ranges for node, deno, and bun when onFail is set to error or warn. Previously these settings only had an effect with onFail: 'download' — the error and warn modes silently did nothing #11818. Violations now throw ERR_PNPM_BAD_RUNTIME_VERSION.
Require provenance before treating trusted publisher metadata as the strongest trust evidence.
Added pnpm stage with publish, list, view, approve, reject, and download subcommands for npm staged publishing.
Added a new setting trustLockfile. When true, pnpm install skips the supply-chain verification pass that re-applies minimumReleaseAge / trustPolicy='no-downgrade' to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author. Defaults to false; verification stays on by default. Set in pnpm-workspace.yaml.
Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (~4k lockfile entries with minimumReleaseAge + trustPolicy: no-downgrade enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (time, per-version _npmUser.trustedPublisher, dist.attestations.provenance). The abbreviated-metadata cache is similarly projected to just the package-level modified field and the set of currently-listed version names. Fixes #11860.
Implemented pnpm pkg command natively, following npm pkg standards.
Implemented pnpm repo command natively, following npm repo standards.
Implemented pnpm set-script (alias ss) natively. Adds or updates an entry in the scripts field of the project manifest, supporting package.json, package.json5, and package.yaml formats.
Add a skip-manifest-obfuscation option for pnpm pack and pnpm publish. When enabled, the original packageManager field and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specific pnpm field continues to be omitted.
Patch Changes
Fixed pnpm dlx failing with ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND when the installed package's CAS slot is missing its package.json. Observed in the wild for pnpm dlx node@runtime:<version> when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship a package.json of their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, so dlx now falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including every runtime: spec) this matches what manifest.bin would have named. Multi-bin packages already require --package=<spec> <bin> to disambiguate and don't enter this code path.
Fixed non-determinism in pnpm dedupe and pnpm install when a dependency graph contains packages with transitive peer dependencies on each other (e.g. @aws-sdk/client-sts and @aws-sdk/client-sso-oidc) and auto-install-peers is enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was that resolveDependencies pushed onto its pkgAddresses / postponedResolutionsQueue arrays from inside Promise.all-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes #8155.
Fixed a regression introduced by #11711 where pnpm add <github-shorthand> (and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs or pnpm/test-git-fetch#sha) was silently dropped from the manifest update and from pendingBuilds. The alias-keyed lookup added in that PR couldn't find a wantedDependency whose alias was undefined at parse time but resolved to a package name only after fetching, so the entry never made it into specsToUpsert. Restored the original index-based pairing between directDependencies and wantedDependencies; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven by rdd.catalogLookup.userSpecifiedBareSpecifier, not by the lookup. Fixes the three rebuilds dependencies / rebuilds specific dependencies / rebuild with pending option failures in building/commands/test/build/index.ts.
Fixed pnpm add --config leaving orphan entries in pnpm-lock.env.yaml (the optional subdependencies of the previously resolved version of the updated config dependency).
Experimental: Adding @pnpm/pacquet (the Rust port of pnpm) to configDependencies in pnpm-workspace.yaml now delegates the materialization phase of pnpm install to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.
To configure pacquet in a project, run:
pnpm add @pnpm/pacquet --config
You'll see changes in pnpm-workspace.yaml and pnpm-lock.yaml that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
Patch Changes
When the install engine is delegated to pacquet via configDependencies, the user's CLI flags passed to pnpm install (e.g. --no-runtime, --prod, --dev, --no-optional, --node-linker, --cpu/--os/--libc, --offline, --prefer-offline) are now forwarded to pacquet's install subcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like --no-runtime were silently dropped. Flag forwarding is gated on the command being install/i; add, update, and dedupe still don't forward (their flag surface doesn't line up with pacquet's install).
Fixed pnpm up (and pnpm add / pnpm remove) failing with pacquet_package_manager::outdated_lockfile when pacquet is declared in configDependencies. pnpm now passes --ignore-manifest-check to pacquet so its --frozen-lockfile check doesn't fire against the (pre-mutation) package.json pnpm hasn't written yet #11797. Requires a pacquet release that supports the flag — bump PACQUET_VERSION in the e2e tests once it ships.
Experimental: Adding @pnpm/pacquet (the Rust port of pnpm) to configDependencies in pnpm-workspace.yaml now delegates the materialization phase of pnpm install to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.
To configure pacquet in a project, run:
pnpm add @pnpm/pacquet --config
You'll see changes in pnpm-workspace.yaml and pnpm-lock.yaml that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
Patch Changes
Mark optional subdependency snapshots of config dependencies with optional: true in the env lockfile, matching how optional dependencies are recorded elsewhere in pnpm-lock.yaml. Previously, snapshots for the platform-specific subdeps pulled in via a config dep's optionalDependencies were written as empty objects, which was inconsistent with the rest of the lockfile and made it look like those non-host platform variants were required.
Fix pickRegistryForPackage returning the wrong registry for an unscoped npm: alias under a scoped local name. A manifest entry like "@private/foo": "npm:lodash@^1" was routing the lodash fetch through registries["@private"], even though lodash is unscoped and doesn't live on that registry. The npm-alias branch now returns the alias target's own scope (or null for an unscoped target, falling through to registries.default) instead of leaking into the local key's scope.
Don't print "Installing config dependencies..." when config dependencies are already installed and nothing needs to be fetched, re-linked, or removed.
Experimental: Adding @pnpm/pacquet (the Rust port of pnpm) to configDependencies in pnpm-workspace.yaml now delegates the materialization phase of pnpm install to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.
To configure pacquet in a project, run:
pnpm add @pnpm/pacquet --config
You'll see changes in pnpm-workspace.yaml and pnpm-lock.yaml that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
configDependencies now resolve and install one level of optionalDependencies declared by the config dependency, with os/cpu/libc platform filtering applied at install time. This unlocks the esbuild/swc-style pattern where a package ships platform-specific binaries via optionalDependencies — a config dependency can now do the same and have the matching binary symlinked next to it in the global virtual store, so require('pkg-platform-arch') from inside the config dependency resolves correctly.
The env lockfile records all platform variants regardless of host platform, so it remains portable across machines. Each entry in a config dependency's optionalDependencies must declare an exact version — ranges and tags are rejected to keep installs reproducible.
Implement the documented pnpm login --scope <scope> flag. The scope is normalized (a leading @ is added if missing; blank values are ignored) and an @<scope>:registry=<registry> mapping is written to the pnpm auth file alongside the auth token. Subsequent installs of @<scope>/* packages then route to the chosen registry. Previously pnpm login --scope foo errored with Unknown option: 'scope' despite the flag being listed in the online documentation #11716.
pnpm outdated and pnpm update --interactive now report Node.js, Deno, and Bun runtimes installed as project dependencies (runtime: specifiers). Previously these were silently skipped.
Patch Changes
Fix cafile=<relative-path> in .npmrc being read from the wrong directory when pnpm is invoked from a different cwd (e.g. pnpm --dir <project> install from a CI wrapper or monorepo script). The path is now resolved against the directory of the .npmrc that declared it, not process.cwd(). Before this fix the CA file silently failed to load — the install proceeded without the configured CA and the user only saw TLS errors against a private registry, with no log line tying back to the wrongly resolved path #11624.
Fix config.registry getting a trailing slash appended when registry is set in .npmrc and no registries.default is provided by pnpm-workspace.yaml. The sync from registries.default to config.registry introduced in #11744 now only fires when the workspace manifest actually contributes a different default.
Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error.
Fix two crashes with injectWorkspacePackages: true when the lockfile has been pruned (e.g. by turbo prune --docker):
Cannot use 'in' operator to search for 'directory' in undefined: a peer-dependency-variant injected snapshot inherits its resolution from the base packages: entry; when a pruner drops that base entry the readers crash. convertToLockfileObject now reconstructs the directory resolution from the file: depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot.
ERR_PNPM_ENOENT on node_modules/.bin/<tool>: after prepare/postinstall, runLifecycleHooksConcurrently re-imported each injected workspace package; the scanDir-into-filesMap workaround fed target-internal paths to the importer, which the makeEmptyDir fast path (#11088) then wiped. Drop the workaround and pass keepModulesDir: true so the importer preserves the target's existing node_modules (bin links + transitive deps) and source files keep their hardlinks.
Fixed pnpm login and pnpm logout ignoring registries.default from pnpm-workspace.yaml#10099.
Fix the minimumReleaseAge (publishedBy) maturity shortcut to be inclusive at the cutoff. Previously, abbreviated metadata whose modified field equalled the cutoff fell off the fast path and triggered a full-metadata re-fetch (or a MISSING_TIME error when full metadata wasn't permitted). Since modified is an upper bound on every version's publish time, modified == publishedBy already implies every version passes the per-version <= filter in filterPkgMetadataByPublishDate, so the shortcut now accepts the boundary case directly. Strictly > (was >=) at the rejection branch.
Honor publishConfig.access when publishing packages.
pnpm install now re-validates pnpm-lock.yaml entries against the active minimumReleaseAge and trustPolicy: 'no-downgrade' policies before any tarball is fetched. Lockfiles resolved elsewhere (committed to the repo, restored from a CI cache, produced by an older pnpm) under a weaker or absent policy can no longer install a freshly-published or trust-downgraded version silently. Violating entries abort the install with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION, ERR_PNPM_TRUST_DOWNGRADE, or the generic ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION when both policies trip in the same batch; minimumReleaseAgeExclude and trustPolicyExclude are honored. Verification results are cached so repeat installs against an unchanged lockfile take a fast path, and pnpm shows a transient progress line while the registry round-trip runs.
When fresh resolution picks an immature version, the behavior depends on minimumReleaseAgeStrict:
Loose mode — the default, in effect whenever minimumReleaseAge keeps its built-in 24-hour value — auto-adds the immature picks to minimumReleaseAgeExclude in pnpm-workspace.yaml and lets the install proceed. A single info message lists what was persisted.
Strict mode in an interactive terminal collects every immature direct AND transitive pick in one pass and prompts once with the full list. Approving adds them to minimumReleaseAgeExclude and the install continues; declining aborts before the lockfile, package.json, or node_modules is touched.
Strict mode in CI (or any non-TTY context) aborts with ERR_PNPM_NO_MATURE_MATCHING_VERSION listing every offending entry, instead of failing on the first one the resolver hit.
minimumReleaseAgeStrict auto-enables whenever the user explicitly sets minimumReleaseAge (CLI flag, env var, global config.yaml, or pnpm-workspace.yaml); set minimumReleaseAgeStrict: false to keep loose-mode auto-collect even with an explicit minimumReleaseAge value. Closes #10438, #10488, #11687.
Allow redundant trailing base64 padding in .npmrc auth values and report invalid auth base64 with a pnpm error.
Make pnpm self-update respect minimumReleaseAge (and minimumReleaseAgeExclude) when resolving which pnpm version to install.
When the latest dist-tag points to a version newer than the configured age threshold, self-update now selects the newest mature version instead unless excluded by minimumReleaseAgeExclude.
Also makes dlx and outdated surface invalid minimumReleaseAgeExclude patterns under the same ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE error code already used by install, instead of leaking the internal ERR_PNPM_INVALID_VERSION_UNION / ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION codes.
Global installs respect global config build policy (e.g., dangerouslyAllowAllBuilds from config.yaml) when GVS is enabled #9249.
The global virtual-store (GVS) default allowBuilds = {} was applied before workspace manifest settings were read and before global config values (stripped by extractAndRemoveDependencyBuildOptions) were re-applied via globalDepsBuildConfig. This caused hasDependencyBuildOptions to return true (because {} is not null), blocking restoration of global config values like dangerouslyAllowAllBuilds. As a result, global installs skipped all build scripts even when the config explicitly allowed them.
This fix moves the GVS default to after workspace manifest reading and globalDepsBuildConfig re-application, so that:
Global config dangerouslyAllowAllBuilds is properly restored (if set and no workspace policy exists)
Empty {} is only applied as a last resort when no policy is configured anywhere
Honor --silent when verifyDepsBeforeRun: install auto-installs dependencies before pnpm run or pnpm exec, preventing install output from being written to stdout #11636.
Fix lockfile parsing failures when pnpm-lock.yaml contains CRLF line endings and multiple YAML documents #11612.
Anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — engines.runtime pin when present, shell node otherwise — instead of pnpm's own runtime.
ENGINE_NAME (the <platform>;<arch>;node<major> prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from process.version — the Node that runs pnpm itself. That was wrong in two situations:
@pnpm/exe SEA bundle. The bundle has its own embedded Node, not the node on the user's PATH that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell node.
engines.runtime / devEngines.runtime pin. When a project pins a Node version via devEngines.runtime (pnpm v11+), pnpm downloads that Node into node_modules/node/ and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node.
Three changes:
@pnpm/engine.runtime.system-node-version now exports engineName(nodeVersion?). Resolves the version in this order: explicit override → getSystemNodeVersion() (which already prefers node --version over process.version in SEA contexts) → process.version.
@pnpm/deps.graph-hasher now exports findRuntimeNodeVersion(snapshotKeys) — scans an iterable of lockfile snapshot keys for a node@runtime:<version> entry and returns its bare version string. calcDepState and calcGraphNodeHash/iterateHashedGraphNodes accept a nodeVersion? (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to engineName(). The default (no override) preserves the pre-change behaviour. The legacy ENGINE_NAME constant in @pnpm/constants is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up.
Every install-side caller of the graph-hasher (@pnpm/installing.deps-resolver, @pnpm/installing.deps-restorer, @pnpm/installing.deps-installer, @pnpm/building.during-install, @pnpm/building.after-install, @pnpm/deps.graph-builder) now derives the project's pinned runtime via findRuntimeNodeVersion(Object.keys(graph)) once per invocation and threads it through.
On upgrade, two one-time GVS slot churns are possible:
SEA-pnpm users without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. node26) now hash under the shell-Node major (e.g. node24), matching what pacquet, the npm-published pnpm package, and any other pnpm-compatible tool already produce.
Projects with a devEngines.runtime pin: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on.
In both cases the old slots become prune-eligible.
Resolve the GVS hash's engine portion per-snapshot when a dependency declares its own engines.runtime, instead of using an install-wide value.
Pnpm's resolver desugars a dep's engines.runtime into dependencies.node: 'runtime:<version>', and the bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into <pkgDir>/node_modules/node/. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so a pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose <platform>;<arch>;node<major> triple disagreed with the Node the build actually ran on.
Per-snapshot resolution now matches what bins/linker already does on a per-package basis:
@pnpm/deps.graph-hasher adds readSnapshotRuntimePin(children) — reads the node entry from one snapshot's graph children and extracts the version from a node@runtime: value. Pairs with the existing findRuntimeNodeVersion(snapshotKeys) install-wide fallback (also now exported from @pnpm/deps.graph-hasher rather than @pnpm/engine.runtime.system-node-version, where it was a poor fit — system-node-version is about probing the host Node, not parsing lockfile-derived strings).
calcDepState and calcGraphNodeHash consult readSnapshotRuntimePin(graph[depPath].children) first and only fall back to the install-wide nodeVersion parameter when the snapshot doesn't pin its own Node.
Pacquet mirrors the same precedence at the calc_graph_node_hash call site in package-manager/src/virtual_store_layout.rs — a new find_own_runtime_node_major(snapshot) helper reads each snapshot's dependencies for a node entry with Prefix::Runtime and overrides the install-wide engine when present.
On upgrade, snapshots of dependencies that declare their own engines.runtime re-hash under that dep's pinned Node instead of the install-wide value. The old slots become prune-eligible. Closes #11690.
Fixed pnpm publish failing with a 404 when authentication relied on OIDC trusted publishing alongside an .npmrc written by actions/setup-node (_authToken=${NODE_AUTH_TOKEN}) without NODE_AUTH_TOKEN being set. Unresolved ${VAR} placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source #11513.
Fix devEngines.packageManager (singular form, without onFail) defaulting to onFail: "error" instead of the documented pmOnFail: "download". As a result, a project that pinned a different pnpm version via devEngines.packageManager and ran pnpm install from a mismatched pnpm version failed with a hard error, even though the migration table from managePackageManagerVersions: true to pmOnFail: download (default) promises the install would auto-download the wanted version #11676.
The array form of devEngines.packageManager keeps its existing per-element defaults (error for the last entry, ignore for the rest), since those reflect explicit prioritization by the user. Explicit onFail values continue to win.
Fix devEngines.packageManager not writing packageManagerDependencies to pnpm-lock.yaml when the lockfile lacks an env-doc entry. Previously the lockfile sync skipped resolution unless an existing packageManagerDependencies.pnpm entry needed refreshing, so a fresh install without onFail: "download" left the resolved pnpm version unrecorded — contradicting the documented behavior that the resolved version is stored in pnpm-lock.yaml#11674.
Warn when package.json contains a legacy pnpm field with settings pnpm no longer reads from package.json (e.g. pnpm.overrides, pnpm.patchedDependencies). Previously these were silently ignored after the upgrade from v10, leaving users unaware that their overrides/patched dependencies had stopped taking effect #11677.
Strip sec-fetch-* headers from outgoing HTTP requests. These headers are automatically added by undici's fetch() implementation per the Fetch spec but cause Azure DevOps Artifacts to return HTTP 400 for uncached upstream packages, as ADO interprets them as browser requests #11572.
Fix minimumReleaseAge handling for cached abbreviated metadata.
The version-spec cache fast path no longer rethrows ERR_PNPM_MISSING_TIME under strictPublishedByCheck; it now falls through to the registry-fetch path, consistent with the adjacent mtime-gated cache block.
When the registry returns 304 Not Modified for a package whose cached metadata is abbreviated (no per-version time), pnpm now re-fetches with fullMetadata: true if minimumReleaseAge is active and the package was modified after the cutoff. The upgraded metadata is persisted to disk so subsequent installs don't repeat the fetch. Previously the abbreviated meta was used as-is and the maturity check fell back to its warn-and-skip path, silently bypassing the quarantine and emitting a misleading "metadata is missing the time field" warning.
Fix pnpm upgrade --interactive --latest -r not respecting named catalog groups. Previously, upgrading a dependency using a named catalog (e.g. "catalog:foo") would incorrectly rewrite package.json to "catalog:" and place the updated version in the default catalog instead of the named one #10115.
Fixed optimisticRepeatInstall skipping pnpm-lock.yaml merge conflict resolution when the existing node_modules state appears up to date.
Fix minimumReleaseAge / resolutionMode: time-based installs failing on lockfiles whose time: block is missing entries. The npm-resolver's peek-from-store fast path now surfaces publishedAt from the lockfile rather than discarding it, and falls through to a registry metadata fetch when the time-based cutoff can't be computed from the data on hand.
Skip installability validation when scanning workspace projects in checkDepsStatus (run by verifyDepsBeforeRun). Previously the status check called findWorkspaceProjects, which validates each project's engines and os/cpu/libc and warns about useless fields in non-root manifests — work that the install pipeline already performs. With no nodeVersion threaded through, the engine check also fell back to the system Node from PATH and emitted spurious "Unsupported engine" warnings before scripts ran. Status-only callers now use findWorkspaceProjectsNoCheck; install paths continue to validate.
Fixed pnpm add <alias>:@scope/pkg for named registries. The local resolver was claiming any specifier containing / as a local directory, so pnpm add bit:@teambit/bit (with bit configured under namedRegistries) installed a bogus link to bit:@teambit/bit/ instead of resolving from the configured registry. The local resolver now runs after the named-registry resolver in the resolution chain.
Updated @zkochan/cmd-shim to 9.0.3. The sh shim it writes for .cmd / .bat targets now escapes the /C switch as //C, so it survives the path translation Git Bash applies when launching cmd.exe. Without this, a bare /C was rewritten to C:\ before reaching cmd.exe — the switch was dropped, cmd started interactively, and the calling script saw the cmd banner instead of the wrapped command's output. Affects any cmd-shim-wrapped batch script invoked from Git Bash / MSYS / Cygwin on Windows. See pnpm/cmd-shim#55.
Added pnpm audit signatures to verify ECDSA registry signatures for installed packages against keys from /-/npm/v1/keys#7909. Scoped registries are respected, and registries without signing keys are skipped.
Added support for installing packages from the GitHub Packages npm registry via a built-in gh: prefix (e.g. pnpm add gh:@acme/private), and, more broadly, for arbitrary named registries in the style of vlt's named-registry aliases. Authentication is picked up from the existing per-URL .npmrc entries (e.g. //npm.pkg.github.com/:_authToken=...), so no separate auth mechanism is required.
Additional aliases — or an override for the built-in gh alias, for GitHub Enterprise Server — can be configured under namedRegistries in pnpm-workspace.yaml:
With this, work:@corp/lib@^2.0.0 resolves against https://npm.work.example.com/. #11324.
Allow setting sbom spec version using --sbom-spec-version#11389.
Add --no-runtime flag (config: runtime=false) to skip installing runtime entries (e.g. Node.js downloaded via devEngines.runtime) without modifying the lockfile. The lockfile keeps the runtime entry so frozen-lockfile validation still passes; only the runtime fetch and .bin linking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. via pnpm runtime -g set node <version>) before pnpm install runs.
Added the pnpm bugs command that opens a package's bug tracker URL in the browser. With no arguments, it reads the current project's package.json; with one or more package names, it fetches each package's metadata from the registry and opens its bug tracker. Falls back to <repository>/issues when the bugs field is missing #11279.
Added pnpm owner command to manage package owners on the registry.
Patch Changes
Added "published X ago by Y" information to the pnpm view command output, similar to npm view. This is useful when comparing against minimumReleaseAge.
For example, pnpm view pnpm now shows:
published 17 hours ago by GitHub Actions
pnpm publish now honors the configured HTTP/HTTPS proxy (including https_proxy/http_proxy/no_proxy environment variables) when polling the registry's doneUrl during the web-based authentication flow. Previously the poll bypassed the proxy, causing the registry to respond 403 from a different source IP and the login to never complete #11561.
pnpm add -g now installs each space-separated package into its own isolated directory by default. To bundle multiple packages into the same isolated install (so that they share dependencies and are removed together), pass them as a comma-separated list. For example:
pnpm add -g foo bar installs foo and bar as two independent globals — removing one does not affect the other.
pnpm add -g foo,bar qar bundles foo and bar into a single isolated install while qar is installed on its own.
pnpm runtime set <name> <version> no longer fails in the root of a multi-package workspace with the ADDING_TO_ROOT error. Installing the workspace root is a valid target for a runtime, so the command now bypasses that safety check.
Fix pnpm --version hanging for the lifetime of the worker pool after the version was printed. main.ts's --version short-circuit returned before reaching the command-handler finally that calls finishWorkers(), so the worker pool that switchCliVersion had spawned during integrity resolution stayed alive and held the Node event loop open. The CLI entry now runs finishWorkers() from its own finally, so every exit path tears the pool down.
Repro: pnpm --version in a workspace whose devEngines.packageManager version already matches the running pnpm + onFail: "download". switchCliVersion resolves the integrity (spawning workers), finds nothing to swap, returns. The version prints, then the process hangs.
Fixed installation of GitLab-hosted dependencies. pnpm now downloads the tarball from https://gitlab.com/<user>/<project>/-/archive/<sha>/<project>-<sha>.tar.gz instead of the GitLab API endpoint that contained an encoded slash (%2F) between user and project. The encoded slash both triggered 406 Not Acceptable responses from GitLab and produced virtual store directory names that Node refused to import (ERR_INVALID_MODULE_SPECIFIER) #11533.
Honor NPM_CONFIG_USERCONFIG (and its lowercase npm_config_userconfig form) as a low-priority fallback when locating the user-level .npmrc. This restores compatibility with environments that point npm at a custom auth file via that env var — most notably actions/setup-node, which writes registry credentials to ${runner.temp}/.npmrc and exports NPM_CONFIG_USERCONFIG to reference it. Without this, GitHub Actions workflows using actions/setup-node to authenticate to private registries broke after upgrading to pnpm v11. PNPM-prefixed env vars and npmrcAuthFile from the global config.yaml continue to take precedence #11539.
Fix pnpm pack not bundling dependencies listed in bundleDependencies (or bundledDependencies). The npm-packlist upgrade in pnpm 11 changed its API to require the caller to pre-populate the dependency tree, which the wrapper was not doing — bundleDependencies were silently dropped from the tarball #11519.
Fixed the pnpm CLI crashing with a confusing SyntaxError: Invalid regular expression flags instead of printing a clear "requires Node.js v22.13" error when launched on an unsupported Node.js version. The Node.js version check in bin/pnpm.mjs was effectively dead code because the static import of the bundled dist/pnpm.mjs was hoisted by the ES module loader and parsed before the check could run #11546.
Fixed pnpm --prefix=<dir> install overwriting the existing pnpm-workspace.yaml in <dir> with set this to true or false placeholders. The renamed --prefix option (which maps to dir) was not honored when locating the workspace root, so the workspace manifest's allowBuilds settings were not loaded into config and got clobbered when ignored builds were auto-populated #11535.
Fixed pnpm publish --provenance failing with a 422 from the registry when the package version contained semver build metadata (e.g. 1.0.0-canary.0+abc1234). The +<build> segment is now stripped before packing so that the version embedded in the tarball, the metadata sent to the registry, and the sigstore provenance subject all agree #11518.
Restored the heuristic that preserves tarball URLs in pnpm-lock.yaml when they cannot be derived from name+version+registry, even with the default lockfileIncludeTarballUrl: false. Without this, pnpm install --frozen-lockfile from an empty store fails with ERR_PNPM_FETCH_404 for packages on registries that serve tarballs from a non-standard path — most notably GitHub Packages (https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>) and JSR. lockfileIncludeTarballUrl: true continues to force the URL into the lockfile for every package #11276.
Run preversion, version, and postversion lifecycle scripts for pnpm version.
Fixed ERR_PNPM_BAD_TARBALL_SIZE when a registry serves tarballs with an end-to-end Content-Encoding (e.g. gzip). Tarballs are already compressed, so the fetcher now requests them with Accept-Encoding: identity (matching pnpm v10's effective behavior) and, as defense in depth against misbehaving servers, no longer enforces the strict Content-Length check when the response declares a Content-Encoding — Content-Length in that case refers to the encoded payload, not the decoded bytes the fetch implementation yields #11506.
Restore the execute bit on the node-gyp shims packed inside @pnpm/exe (dist/node-gyp-bin/node-gyp, dist/node-gyp-bin/node-gyp.cmd, and dist/node_modules/node-gyp/bin/node-gyp.js). Without this, pnpm/action-setup's standalone path (used on runners with Node.js < 22.13) failed any install whose lifecycle script invoked node-gyp rebuild with sh: 1: node-gyp: Permission denied#11483.
Fixed the pn, pnpx, and pnx aliases failing in Git Bash / MSYS2 on Windows when pnpm was installed via @pnpm/exe (or after pnpm self-update) #11486. Running pnpx (or pnx) printed the cmd.exe banner and dropped the user into an interactive command prompt instead of running pnpm dlx. The bin field rewrite on Windows was pointing those aliases at .cmd files; cmd-shim's Bash shim for a .cmd target wraps it in exec cmd /C ..., and MSYS2 mangles /C into a Windows path before cmd.exe sees it. The aliases are now .exe hardlinks of the SEA binary, which detects which name it was launched as via process.execPath and prepends dlx for pnpx / pnx.
Fix pnpm install recreating node_modules after pnpm fetch. pnpm fetch records empty hoistPattern and publicHoistPattern in .modules.yaml; since v11 removed the explicit-config gate, the follow-up install treated those as a hoist-pattern change and purged the modules directory. The fetch step now flags the modules manifest with virtualStoreOnly: true so the next install skips the hoist-pattern comparison and completes the missing post-import linking in place #11488.
Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
A new gitHosted: true field is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.
Allow user-level preferences in the global config.yaml. The following settings can now be set in ~/.config/pnpm/config.yaml (or via pnpm config set --location global) instead of being restricted to pnpm-workspace.yaml: agent, globalVirtualStoreDir, initPackageManager, initType, registrySupportsTimeField, scriptShell, shellEmulator, sideEffectsCache, sideEffectsCacheReadonly, stateDir, strictDepBuilds, trustPolicy, trustPolicyExclude, trustPolicyIgnoreAfter, updateNotifier, useStderr, verifyDepsBeforeRun, verifyStoreIntegrity, virtualStoreDir, virtualStoreDirMaxLength#11474.
Make trusted publishing (OIDC) take precedence over a configured static _authToken in pnpm publish, mirroring the npm CLI's behavior. When OIDC succeeds, the OIDC-derived token overrides any pre-configured _authToken; when OIDC is not applicable (no CI environment, exchange fails, registry has no trusted publisher configured), the static token is used as a fallback. This applies on every package during recursive publish, so each workspace package independently attempts trusted publishing.
Additionally, the NPM_ID_TOKEN env var is now honored as a CI-agnostic injection point for an OIDC ID token. Previously OIDC was only attempted on GitHub Actions or GitLab; now any CI provider that exposes its own OIDC mechanism (e.g. CircleCI's CIRCLE_OIDC_TOKEN_V2, Buildkite, etc.) can forward its token via NPM_ID_TOKEN and trusted publishing will work without pnpm needing to recognize the provider explicitly.
--pm-on-fail=ignore (and other universal options like --loglevel, --reporter) is now honored when combined with --help or --version. Previously the CLI argument parser short-circuited those flags before universal options were preserved, so pnpm audit --pm-on-fail=ignore --help and pnpm --pm-on-fail=ignore --version reported the strict packageManager mismatch instead of running the requested action #11487.
Fix a regression where pnpm --recursive --filter '!<pkg>' run/exec/test/add would include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative --filter arguments are provided, matching the documented behavior. To include the root, pass --include-workspace-root#11341.
Restore npm-CLI-compatible --json stdout output for pnpm publish (#11476). pnpm 11 reimplemented publish natively (#10591) and inadvertently dropped the per-package JSON object that pnpm 10 emitted transitively via the npm CLI, silently breaking downstream tooling — most notably nx release publish, which parses stdout JSON to confirm success (nrwl/nx#35575). On success, the output is now:
pnpm publish -r --json → array of those objects, mirroring pnpm pack --json's shape choice.
pnpm publish -r --report-summary → existing pnpm-publish-summary.json envelope { publishedPackages: [...] } is preserved, but each entry is upgraded to the same per-package shape (additive — name and version are still present).
pnpm config get @<scope>:registry now reports the same URL that pnpm publish and the resolvers actually use. Previously, config get only consulted .npmrc, while publish/install used the merged map that includes pnpm-workspace.yaml's registries block — so the two could diverge silently and a publish could go to the wrong registry #11492.
Fix pnpm_config_npmrc_auth_file and pnpm_config_userconfig env vars not actually loading the custom .npmrc. The env vars were parsed and assigned to the resolved config, but only after loadNpmrcConfig had already read the default ~/.npmrc — so the custom file path was set but never read. The relevant env vars are now consulted before the user-level .npmrc is loaded #11465.
Preserve the original key order in pnpm-workspace.yaml when updating it. Existing keys keep their position, and new keys are inserted in alphabetical position when the existing keys are already sorted (with a leading packages key allowed) or appended at the end otherwise.
Fixed pnpm self-update on installations originally set up by pnpm v10. v10 added PNPM_HOME directly to PATH and wrote a pnpm bootstrap shim there. v11 setup writes shims under PNPM_HOME/bin instead, so when a v10 user upgrades to v11 the legacy shim at PNPM_HOME keeps pointing into the old .tools/<version> install — pnpm --version continues to report the pre-update version even though the new version was installed under global/v11. Self-update now detects this layout, refreshes the legacy shims so the upgrade actually takes effect, and prints a hint suggesting pnpm setup to migrate PATH to the v11 layout. #11464.
Print a warning when settings that are not allowed in the global config file (e.g. nodeLinker, hoistPattern) are present in config.yaml and silently ignored. Previously these settings were dropped without any feedback, leaving users unsure why their global configuration had no effect. The warning suggests moving those settings to a project-level pnpm-workspace.yaml, or sharing them across projects via config dependencies.
Throw a pnpm error when overrides has an invalid shape or contains a non-string value.
Validate all readPackage dependency map fields, including devDependencies, and reject falsy non-object invalid values instead of silently accepting them.
Prevent crashes during pnpm config, pnpm set, and pnpm get by tolerating configDependencies install failures. For these commands, a failure to install configDependencies (for example because the registry auth token has not been written yet) is now logged at debug level and the command proceeds. All other commands still surface the install error #10684.
Treat allowBuilds as an install-state input and clear previously ignored builds when they are explicitly disallowed.
Fixes #10594, catalogs not being read from the workspace when using the catalog: protocol with the pnpm dlx / pnpx command, resulting in a catalog entry not found error.
Accept PNPM_CONFIG_* (uppercase) environment variables in addition to pnpm_config_*. Previously, only the lowercase form was honored, so env vars renamed per the v11 migration guide (e.g. PNPM_CONFIG_USERCONFIG) silently had no effect on case-sensitive systems like macOS and Linux #11465.
Drop the darwin-x64 artifact from @pnpm/exe and from the GitHub release page. The Node.js SEA mechanism pnpm pack-app uses produces a binary that segfaults at startup on Intel Macs because of an upstream Node.js bug (nodejs/node#62893, tracked alongside #59553; the Node.js team has opted not to fix it on the grounds that x64 macOS is being phased out). Re-signing with codesign or ldid doesn't help — the corruption is in LIEF's Mach-O surgery, before signing.
Intel Mac users should install pnpm via npm install -g pnpm (uses the system Node.js, no SEA), or stay on pnpm 10.x. @pnpm/exe's preinstall on Intel Mac now exits with a clear error pointing at these alternatives.
pnpm dlx (and pnpx/pnx/pnpm create) now runs the same interactive approve-builds prompt as pnpm add -g when the package being launched depends on transitive packages with install scripts. Previously, the v11 strictDepBuilds default made dlx fail with ERR_PNPM_IGNORED_BUILDS and required users to re-run with --allow-build=<pkg> for every offending dependency. dlx also now removes the partially-populated cache directory when the install fails, so a subsequent run starts clean instead of reusing a broken install whose builds were silently skipped #11444.
72629fc: Fix pnpm -g ls --json and pnpm -g ls --parseable so they emit valid JSON and parseable output respectively, matching pnpm 10 behavior. Since the isolated global packages refactor in pnpm 11, the global list command had a custom path that always printed plain text and ignored --json/--parseable, which broke tools like npm-check-updates that parse the JSON output #11440.
pnpm -g ls --depth=<n> (with n > 0) now errors when more than one isolated global install would be involved, since each install has its own lockfile and merging their transitive trees would be incoherent. When the request can be narrowed to a single install group, the regular list flow is used and the full dependency tree is shown.
Fixed pnpm publish to honor publishConfig.registry from package.json when publishing a single package. The native publish flow introduced in v11 was reading the registry from .npmrc only, ignoring the per-package override #11419.
When strictPeerDependencies is true, the ERR_PNPM_PEER_DEP_ISSUES error once again renders the peer dependency issues inline using the same format as pnpm peers check, so users (and CI tools like Renovate) can see what failed without running pnpm peers check separately #11439.
The WARN and error code labels in pnpm's output now wrap in brackets ([WARN], [ERR_PNPM_FOO]). Previously the labels relied entirely on a colored background to stand out, which meant they blended into the surrounding text in terminals without color (e.g. when NO_COLOR is set or output is piped). The brackets are painted in the same color as the badge background, so they appear as ordinary padding in color-capable terminals — only the no-color rendering changes.
Fixed pnpm ci not reinstalling workspace package node_modules directories after the clean step #11427.
Remove pnpm's workspace state file when cleaning node_modules so pnpm ci performs a fresh install after the clean step.
Do not remove pnpm-lock.yaml during pnpm clean when lockfile: true is configured in pnpm-workspace.yaml. The lockfile is only removed when the --lockfile option is passed to pnpm clean.
pnpm self-update (with no version argument) no longer downgrades pnpm when the registry's latest dist-tag points to an older release than the currently active version. Run pnpm self-update latest to force a downgrade #11418.
minimumReleaseAgeStrict now defaults to true whenever the user explicitly sets minimumReleaseAge (via pnpm-workspace.yaml, the global config.yaml, the CLI, or pnpm_config_* env vars).
Fix too many open files error sometimes happening on Windows, when creating command shims in node_modules/.bin#11412.
Fix ERR_PNPM_FETCH_404 when installing a project whose lockfile depends on a file: tarball. The previous behavior dropped the tarball field from file: and git-hosted resolutions when lockfile-include-tarball-url=false (the default), even though those URLs cannot be reconstructed from the package name, version, and registry #11407.
Fix ENOENT symlink failure when pnpm add -g triggers the approve-builds prompt. The global add flow used to forward an absolute modulesDir (<installDir>/node_modules) into the install run by approve-builds. The install layer treated modulesDir as a path relative to lockfileDir and joined it again, producing a doubled path on Windows because path.join does not collapse an embedded absolute path. The hoist step then tried to mkdir and symlink under <installDir>\<installDir>\node_modules\.pnpm\node_modules\... and failed with ENOENT#11403.
Fixed packageManagerDependencies going stale when pnpm is invoked through corepack. The lockfile sync (and the devEngines.packageManager version check) previously ran only when pnpm was invoked directly; under corepack the entire block was skipped, so a stale entry would persist even after the running pnpm version changed. The lockfile sync now runs regardless of how pnpm was invoked, while the pnpm-managed version switch (onFail: 'download') remains skipped under corepack so it doesn't fight corepack's own version selection #11397.
Fix recursive publish summaries to report the manifest from publishConfig.directory when packages publish from a generated directory #11239.
Fix negated os / cpu entries (e.g. ["!win32"]) being incorrectly rejected when supportedArchitectures expands to multiple platforms #11375.
Report unknown top-level options before falling back to implicit pnpm run scripts.
Reject null named catalogs in workspace manifests with InvalidWorkspaceManifestError instead of crashing with a raw TypeError.
Populate download location for git-sourced dependencies in SBOM output. Previously pnpm sbom emitted NOASSERTION (SPDX) and omitted the distribution reference (CycloneDX) for git dependencies. Now emits the git URL with commit hash, e.g. git+https://github.com/user/repo.git#commit.
pnpm self-update now keeps package.json's packageManager and devEngines.packageManager in sync. When the legacy packageManager field pins pnpm, both fields are rewritten to the new exact pnpm version on update — packageManager to pnpm@<version> (without an integrity hash), and devEngines.packageManager.version to the same exact <version> (dropping any range operator). When only devEngines.packageManager is declared, the existing range-preserving behavior is unchanged #11388.
Sort the keys of the overrides object returned by pnpm audit --fix so that the log output order matches the order written to pnpm-workspace.yaml.
Update the env lockfile's packageManagerDependencies entry when devEngines.packageManager declares a pnpm version that the lockfile no longer satisfies. Previously, the stale entry was kept even though the running pnpm matched the declared version, silently breaking the integrity record #11387.
Node.js 22+ required — support for Node 18, 19, 20, and 21 is dropped, pnpm itself is now pure ESM, and the standalone exe requires glibc 2.27.
Supply-chain protection on by default — minimumReleaseAge defaults to 1 day (newly published packages are not resolved for 24h) and blockExoticSubdeps defaults to true.
allowBuilds replaces the old build-dependency settings — onlyBuiltDependencies, onlyBuiltDependenciesFile, neverBuiltDependencies, ignoredBuiltDependencies, and ignoreDepScripts have been removed.
Global installs are isolated and use the global virtual store by default — each pnpm add -g gets its own directory with its own package.json, node_modules, and lockfile.
New SQLite-backed store index (store v11) with bundled manifests and hex digests, reducing filesystem syscalls and speeding up installation.
pnpm audit uses npm's bulk advisories endpoint — the legacy /security/audits endpoints are gone. CVE-based filtering has been replaced with GHSA-based filtering: migrate auditConfig.ignoreCves entries to auditConfig.ignoreGhsas.
.npmrc is auth/registry only — all other settings must live in pnpm-workspace.yaml or the new global config.yaml, and environment variables use the pnpm_config_* prefix.
Runtime installs are slimmer — installing a Node.js runtime via node@runtime:<version> no longer extracts the bundled npm, npx, and corepack, roughly halving the files pnpm has to hash, write, and link.
ESM pnpmfiles via .pnpmfile.mjs, which takes priority over .pnpmfile.cjs when present.
pnpm audit --fix=update fixes vulnerabilities by updating packages in the lockfile instead of adding overrides, and pnpm audit --fix --interactive lets you select which advisories to fix.
pnpm pack-app packs a CommonJS entry into a standalone executable for one or more target platforms using Node.js Single Executable Applications.
Faster HTTP and I/O — undici with Happy Eyeballs, direct-to-CAS writes, skipped staging directory, pre-allocated tarball downloads, and an NDJSON metadata cache.
Major Changes
Requirements
pnpm is now distributed as pure ESM.
Dropped support for Node.js v18, 19, 20, and 21.
The standalone exe version of pnpm requires at least glibc 2.27.
Security & Build Defaults
Changed default values: optimisticRepeatInstall is now true, verifyDepsBeforeRun is now install, minimumReleaseAge is now 1440 (1 day), and minimumReleaseAgeStrict is false. Newly published packages will not be resolved until they are at least 1 day old. This protects against supply chain attacks by giving the community time to detect and remove compromised versions. To opt out, set minimumReleaseAge: 0 in pnpm-workspace.yaml#11158.
Use the allowBuilds setting instead. It is a map where keys are package name patterns and values are booleans:
true means the package is allowed to run build scripts
false means the package is explicitly denied from running build scripts
Same as before, by default, none of the packages in the dependencies are allowed to run scripts. If a package has postinstall scripts and it isn't declared in allowBuilds, an error is printed.
Removed allowNonAppliedPatches in favor of allowUnusedPatches.
Removed ignorePatchFailures; patch application failures now throw an error.
Store
Runtime dependencies are always linked from the global virtual store #10233.
Optimized index file format to store the hash algorithm once per file instead of repeating it for every file entry. Each file entry now stores only the hex digest instead of the full integrity string (<algo>-<digest>). Using hex format improves performance since file paths in the content-addressable store use hex representation, eliminating base64-to-hex conversion during path lookups.
Store version bumped to v11.
The bundled manifest (name, version, bin, engines, scripts, etc.) is now stored directly in the package index file, eliminating the need to read package.json from the content-addressable store during resolution and installation. This reduces I/O and speeds up repeat installs #10473.
The package index in the content-addressable store is now backed by SQLite. Instead of individual JSON files under $STORE/index/, package metadata is stored in a single SQLite database at $STORE/index.db with MessagePack-encoded values. This reduces filesystem syscall overhead, improves space efficiency for small metadata entries, and enables concurrent access via SQLite's WAL mode. Packages missing from the new index are re-fetched on demand #10500#10826.
Global Packages
Global installs (pnpm add -g pkg) and pnx now use the global virtual store by default. Packages are stored at {storeDir}/links instead of per-project .pnpm directories. This can be disabled by setting enableGlobalVirtualStore: false#10694.
Isolated global packages. Each globally installed package (or group of packages installed together) now gets its own isolated installation directory with its own package.json, node_modules/, and lockfile. This prevents global packages from interfering with each other through peer dependency conflicts, hoisting changes, or version resolution shifts.
Key changes:
pnpm add -g <pkg> creates an isolated installation in {pnpmHomeDir}/global/v11/{hash}/
pnpm remove -g <pkg> removes the entire installation group containing the package
pnpm update -g [pkg] re-installs packages in new isolated directories
pnpm list -g scans isolated directories to show all installed global packages
pnpm install -g (no args) is no longer supported; use pnpm add -g <pkg> instead
Globally installed binaries are now stored in a bin subdirectory of PNPM_HOME instead of directly in PNPM_HOME. This prevents internal directories like global/ and store/ from polluting shell autocompletion when PNPM_HOME is on PATH #10986. After upgrading, run pnpm setup to update your shell configuration.
Breaking changes to pnpm link:
pnpm link <pkg-name> no longer resolves packages from the global store. Only relative or absolute paths are accepted. For example, use pnpm link ./foo instead of pnpm link foo.
pnpm link --global is removed. Use pnpm add -g . to register a local package's bins globally.
pnpm link (no arguments) is removed. Use pnpm link <dir> with an explicit path instead.
Configuration
pnpm no longer reads all settings from .npmrc. Only auth and registry settings are read from .npmrc files. All other settings (like hoistPattern, nodeLinker, shamefullyHoist, etc.) must be configured in pnpm-workspace.yaml or the global ~/.config/pnpm/config.yaml#11189.
Network settings (httpProxy, httpsProxy, noProxy, localAddress, strictSsl, gitShallowHosts) are now written to config.yaml (global) or pnpm-workspace.yaml (local) instead of .npmrc/auth.ini. They are still readable from .npmrc for easier migration from the npm CLI #11209.
pnpm no longer reads npm_config_* environment variables. Use pnpm_config_* environment variables instead (e.g., pnpm_config_registry instead of npm_config_registry).
pnpm no longer reads the npm global config at $PREFIX/etc/npmrc.
pnpm login writes auth tokens to ~/.config/pnpm/auth.ini.
Auth tokens in ~/.npmrc still work — pnpm continues to read ~/.npmrc as a fallback for registry authentication. The new npmrcAuthFile setting can be used to point to a different file instead of ~/.npmrc.
Replace workspace project specific .npmrc with packageConfigs in pnpm-workspace.yaml.
A workspace manifest with packageConfigs looks something like this:
pnpm no longer reads settings from the pnpm field of package.json. Settings should be defined in pnpm-workspace.yaml#10086.
pnpm config get (without --json) no longer prints INI formatted text. Instead, it prints JSON for objects and arrays, and raw strings for strings, numbers, booleans, and nulls. pnpm config get --json still prints all types of values as JSON, as before.
pnpm config get <array> now prints a JSON array.
pnpm config list now prints a JSON object instead of INI formatted text.
pnpm config list and pnpm config get (without argument) now hide auth-related settings.
pnpm config list and pnpm config get (without argument) now show top-level keys as camelCase. Exception: keys that start with @ or // are preserved (their cases don't change).
pnpm config get and pnpm config list no longer load non-camelCase options from the workspace manifest (pnpm-workspace.yaml).
Removed Commands & npm Passthrough
pnpm no longer falls back to the npm CLI. Commands that were previously passed through to npm (access, bugs, docs, edit, find, home, issues, owner, ping, prefix, profile, pkg, repo, search, set-script, star, stars, team, token, unstar, whoami, xmas) and their aliases (s, se) now throw a "not implemented" error, with a suggestion to use the npm CLI directly #10642. Other previously passed-through commands — view (info, show, v), login (adduser), logout, deprecate, unpublish, dist-tag, and version — have been reimplemented natively in pnpm (see New Commands below).
If the registry requests OTP and the user has not provided it via the PNPM_CONFIG_OTP environment variable or the --otp flag, pnpm will prompt the user directly for an OTP code.
If the registry requests web-based authentication, pnpm will print a scannable QR code along with the URL.
Since the new pnpm publish no longer calls npm publish, some undocumented features may have been unknowingly dropped. If you rely on a feature that is now gone, please open an issue at https://github.com/pnpm/pnpm/issues. In the meantime, you can use pnpm pack && npm publish *.tgz as a workaround.
Removed support for the useNodeVersion and executionEnv.nodeVersion fields. devEngines.runtime and engines.runtime should be used instead #10373.
Removed support for hooks.fetchers. We now have a new API for custom fetchers and resolvers via the fetchers field of pnpmfile.
Lifecycle Scripts
pnpm no longer populates npm_config_* environment variables from the pnpm config during lifecycle scripts. Only well-known npm_* env vars are now set, matching Yarn's behavior #11116.
CLI Output
Cleaner output for script execution: pnpm now prints $ command instead of > pkg@version stage path\n> command, and shows project name and path only when running in a different directory. The $ command line is printed to stderr to keep stdout clean for piping #11132.
During install, instead of rendering the full peer dependency issues tree, pnpm now suggests running pnpm peers check to view the issues #11133.
Lockfile
Simplified patchedDependencies lockfile format from Record<string, { path: string, hash: string }> to Record<string, string> (selector to hash). Existing lockfiles with the old format are automatically migrated #10911.
Other
The default value of the type field in the package.json file of the project initialized by pnpm init command has been changed to module.
Added support for lowercase options in pnpm add: -d, -p, -o, -e#9197.
When using the pnpm add command only:
-p is now an alias for --save-prod instead of --parseable
-d is now an alias for --save-dev instead of --loglevel=info
The root workspace project is no longer excluded when it is explicitly selected via a filter #10465.
Audit
pnpm audit now calls npm's /-/npm/v1/security/advisories/bulk endpoint. The legacy /-/npm/v1/security/audits{,/quick} endpoints have been retired by the registry, so the legacy request/response contract is no longer supported.
The bulk endpoint does not return CVE identifiers. CVE-based filtering has been replaced with GitHub advisory ID (GHSA) filtering:
auditConfig.ignoreCves → auditConfig.ignoreGhsas (the previous key is no longer recognized)
pnpm audit --ignore <id> / pnpm audit --ignore-unfixable now read and write GHSAs instead of CVEs
GHSAs are derived from each advisory's url (https://github.com/advisories/GHSA-xxxx-xxxx-xxxx)
To migrate: replace each CVE-YYYY-NNNNN entry in your auditConfig.ignoreCves with the corresponding GHSA-xxxx-xxxx-xxxx value (visible in the More info column of pnpm audit output) and move it under auditConfig.ignoreGhsas.
Package Manager Settings
Breaking: removed the managePackageManagerVersions, packageManagerStrict, and packageManagerStrictVersion settings. They existed only to derive the onFail behavior for the legacy packageManager field, and the pmOnFail setting introduced alongside pnpm with subsumes all three — it directly sets the onFail behavior of both packageManager and devEngines.packageManager. The COREPACK_ENABLE_STRICT environment variable is no longer honored (it only gated packageManagerStrict); use pmOnFail instead.
Migration:
Removed setting
Replace with
managePackageManagerVersions: true
pmOnFail: download (default)
managePackageManagerVersions: false
pmOnFail: ignore
packageManagerStrict: false
pmOnFail: warn
packageManagerStrictVersion: true
pmOnFail: error
COREPACK_ENABLE_STRICT=0
pmOnFail: warn
Runtime Installs
Installing a Node.js runtime via node@runtime:<version> (including pnpm env use and pnpm runtime set node) no longer extracts the bundled npm, npx, and corepack from the Node.js archive. This cuts roughly half of the files pnpm has to hash, write to the CAS, and link during installation, making runtime installs noticeably faster. Users who still need npm can install it as a separate package.
Minor Changes
New Commands
Added native pnpm view (info, show, v) command for viewing package metadata from the registry #11064.
Added pnpm login (and pnpm adduser alias) command for authenticating with npm registries. Supports web-based login with QR code as well as classic username/password login #11094.
Added pnpm logout command for logging out of npm registries. Revokes the authentication token on the registry and removes it from the local auth config file #11213.
Added native pnpm deprecate and pnpm undeprecate commands for setting and removing deprecation messages on package versions without delegating to the npm CLI #11120.
Added native pnpm unpublish command. Supports unpublishing specific versions, version ranges via semver, and entire packages with --force#11128.
Added pnpm sbom command for generating Software Bill of Materials in CycloneDX 1.7 and SPDX 2.3 JSON formats #9088.
Added pnpm clean command that safely removes node_modules directories from all workspace projects #10707. Use --lockfile to also remove pnpm-lock.yaml files.
Added the ability to fix vulnerabilities by updating packages in the lockfile instead of adding overrides. Use pnpm audit --fix=update#10341.
Added pnpm ci command for clean installs #6100. The command runs pnpm clean followed by pnpm install --frozen-lockfile. Designed for CI/CD environments where reproducible builds are critical. Aliases: pnpm clean-install, pnpm ic, pnpm install-clean#11003.
Added pnpm peers check command that checks for unmet and missing peer dependency issues by reading the lockfile #7087.
Implemented the version command natively in pnpm to support workspaces and workspace: protocols correctly. The new command allows bumping package versions (major, minor, patch, etc.) with full workspace support and git integration #10879.
pnpm audit --fix now supports a new interactive mode via --interactive/-i.
Added the pnpm docs command and its alias pnpm home. This command opens the package documentation or homepage in the browser. When the package has no valid homepage, it falls back to https://npmx.dev/package/<name>.
Added native pnpm ping command to test registry connectivity. Provides a simple way to verify connectivity to the configured registry without requiring external tools.
Implemented native search command and its aliases (s, se, find).
Add pnpm with <version|current> <args...> command. Runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project's packageManager and devEngines.packageManager pins.
Added support for a global YAML config file named config.yaml.
Configuration is now split into two categories:
Registry and auth settings, which can be stored in INI files such as the global rc file and local .npmrc.
pnpm-specific settings, which can only be loaded from YAML files such as the global config.yaml and local pnpm-workspace.yaml.
Added support for loading environment variables whose names start with pnpm_config_ into config. These environment variables override settings from pnpm-workspace.yaml but not CLI arguments.
Added support for reading allowBuilds from pnpm-workspace.yaml in the global package directory for global installs.
Added support for pnpm config get globalconfig to retrieve the global config file path #9977.
Added a new setting virtualStoreOnly that populates the virtual store without creating importer symlinks, hoisting, bin links, or running lifecycle scripts. This is useful for pre-populating a store (e.g., in Nix builds) without creating unnecessary project-level artifacts. pnpm fetch now uses this mode internally #10840.
Added support for specifying the pnpm version via devEngines.packageManager in package.json. Unlike the packageManager field, this supports version ranges. The resolved version is stored in pnpm-lock.yaml and reused if it still satisfies the range #10932.
Added a new dedupePeers setting that reduces peer dependency duplication. When enabled, peer dependency suffixes use version-only identifiers (name@version) instead of full dep paths, eliminating nested suffixes like (foo@1.0.0(bar@2.0.0)). This dramatically reduces the number of package instances in projects with many recursive peer dependencies #11070.
Config dependencies are now installed into the global virtual store ({storeDir}/links/) and symlinked into node_modules/.pnpm-config/. This allows config dependencies to be shared across projects that use the same store, avoiding redundant fetches and imports #10910. Config dependency and package manager integrity info is now stored in pnpm-lock.yaml instead of inlined in pnpm-workspace.yaml: the workspace manifest contains only clean version specifiers for configDependencies, while the resolved versions, integrity hashes, and tarball URLs are recorded in the lockfile as a separate YAML document. The env lockfile section also stores packageManagerDependencies resolved during version switching and self-update. Projects using the old inline-hash format are automatically migrated on install #10912#10964.
Added nodeDownloadMirrors setting to configure custom Node.js download mirrors in pnpm-workspace.yaml. This replaces the node-mirror:<channel>.npmrc setting, which is no longer read #11194:
pnpm dlx and pnpm create now respect security and trust policy settings (minimumReleaseAge, minimumReleaseAgeExclude, minimumReleaseAgeStrict, trustPolicy, trustPolicyExclude, trustPolicyIgnoreAfter) from project-level configuration #11183.
pnpm init now writes a devEngines.packageManager field instead of the packageManager field when init-package-manager is enabled.
Added a new setting runtimeOnFail that overrides the onFail field of devEngines.runtime (and engines.runtime) in the root project's package.json. Accepted values: ignore, warn, error, download. For example, setting runtimeOnFail=download makes pnpm download the declared runtime version even when the manifest does not set onFail: "download".
Added a new setting minimumReleaseAgeIgnoreMissingTime, which is true by default. When enabled, pnpm skips the minimumReleaseAge maturity check if the registry metadata does not include the time field. Set to false to fail resolution instead.
Store
When the global virtual store is enabled, packages that are not allowed to build (and don't transitively depend on packages that are) now get hashes that don't include the engine name (platform, architecture, Node.js major version). This means ~95% of packages in the GVS survive Node.js upgrades and architecture changes without re-import #10837.
Hooks & Pnpmfiles
Added support for pnpmfiles written in ESM, using the .mjs extension. When .pnpmfile.mjs exists, it takes priority over .pnpmfile.cjs and only one is loaded #9730.
CLI & Other
The built-in clean, setup, deploy, and rebuild commands now prefer user scripts over built-in commands. When a project's package.json has a script with the same name, pnpm executes the script instead of the built-in command. Added purge as an alias for the built-in clean command, which always runs the built-in regardless of scripts #11118.
Added -F as a short alias for the --filter option.
Added support for hidden scripts. Scripts starting with . are hidden and cannot be run directly via pnpm run. They can only be called from other scripts. Hidden scripts are also omitted from the pnpm run listing #11041.
pnpm approve-builds now accepts positional arguments for approving or denying packages without the interactive prompt. Prefix a package name with ! to deny it. Only mentioned packages are affected; the rest are left untouched #11030.
During install, packages with ignored builds that are not yet listed in allowBuilds are automatically added to pnpm-workspace.yaml with a placeholder value, so users can manually set them to true or false#11030.
Added pn and pnx short aliases for pnpm and pnpx (pnpm dlx) #11052.
pnpm store prune now displays the total size of removed files #11047.
pnpm audit --fix now adds the minimum patched version for each advisory to minimumReleaseAgeExclude in pnpm-workspace.yaml, so the security fix can be installed without waiting for minimumReleaseAge#11216.
pnpm now warns when optimisticRepeatInstall skips shouldRefreshResolution hooks #10995.
Performance
Replaced node-fetch with native undici for HTTP requests throughout pnpm #10537.
Eliminated redundant internal linking during GVS warm reinstall when no packages were added #11073.
Eliminated the staging directory when importing packages into node_modules, avoiding the overhead of creating a temp dir and renaming per package #11088.
CAS files are now written directly to their final content-addressed path instead of to a temp file and renamed. This eliminates ~30k rename syscalls per cold install #11087.
Optimized hot-path string operations in the content-addressable store and increased gunzipSync chunk size for fewer buffer allocations during tarball decompression #11086.
Improved HTTP performance with Happy Eyeballs (dual-stack), better keep-alive settings, and an optimized global dispatcher. Tarball downloads with known size now pre-allocate memory to avoid double-copy overhead #11151.
Switched to abbreviated metadata when checking minimumReleaseAge, reducing the amount of data fetched from the registry #11160.
Switched the metadata cache to NDJSON format, improving read/write performance #11188.
Patch Changes
Switched to process.stderr.write instead of console.error for script logging #11140.
Respected the frozen-lockfile flag when migrating config dependencies #11067.
Removed the --workspace flag from the version command #11115.
Handled ENOTSUP error in the clone import path during parallel I/O #11117.
Fixed pnpm audit command.
Updated dependencies to fix vulnerabilities.
pnpm now checks whether a package is installable for non-npm-hosted packages (e.g., git or tarball dependencies) after the manifest has been fetched.
pnpm now explicitly passes the path of the global rc config file to npm.
Fixed YAML formatting preservation in pnpm-workspace.yaml when running commands like pnpm update. Previously, quotes and other formatting were lost even when catalog values didn't change.
The parameter set by the --allow-build flag is now written to allowBuilds.
Fixed a bug in which specifying filter in pnpm-workspace.yaml would cause pnpm to not detect any projects.
Deferred patch errors until all patches in a group are applied, so that one failed patch does not prevent other patches from being attempted.
pnpm now fails on incompatible lockfiles in CI when frozen lockfile mode is enabled #10978.
Fixed strictDepBuilds and allowBuilds checks being bypassed when a package's build side-effects are cached in the store #11039.
In GVS mode, pnpm approve-builds now runs a full install instead of rebuild, ensuring that GVS hash directories and symlinks are updated correctly after changing allowBuilds#11043.
Fixed a crash in the lockfile merger when merging non-semver version strings (e.g. link:, file:, git URLs) #11102.
Handled ENOTSUP error in linkOrCopy during parallel imports #11103.
Skipped linking bins that already reference the correct target. This avoids redundant I/O during repeated installs and prevents permission errors when the store is read-only (e.g. Docker layer caching, CI prewarm, NFS) #11069.
Fixed _password handling for the default registry to decode from base64 before use, consistent with scoped registry behavior #11089.
Fixed a bug where the CAS locker cache was not updated when a file already existed with correct integrity #11085.
Prevented catalog entries from being removed by cleanupUnusedCatalogs when they are referenced only from workspace overrides#11075.
Resolved patch file paths during pnpm fetch#11054.
Fixed invalid specifiers for peers on all non-exact version selectors #11049.
Fixed false "Command not found" error on Windows when the command exists but exits with a non-zero exit code #11000.
Prepended Bearer to the authorization token generated by tokenHelper if it is missing, aligning with npm's behavior #11097.
Propagated error cause when throwing PnpmError in @pnpm/npm-resolver#10990.
Fixed SQLite race condition during store initialization on Windows.
Removed rimrafSync in importIndexedDir fast-path error handler #11168.
Fixed pnpm dedupe --check unexpectedly failing due to non-deterministic resolution #11110.
Fixed empty files not being rejected in isEmptyDirOrNothing#11182.
Fixed .bat/.cmd token helpers not working on Windows due to missing shell: true option.
78e29fe: Prevent a crafted pnpm-lock.yaml from writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g. ../../../tmp/x@1.0.0) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshot version: "../../x") is now rejected at iterateHashedGraphNodes, the single point every global-virtual-store slot path funnels through.
78e29fe: Fixed a path traversal vulnerability where a dependency whose manifest name was a scoped path traversal (e.g. @x/../../../<path>) could be written outside node_modules to an attacker-controlled location during pnpm install, even with --ignore-scripts. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.
47ef6f0: Fixed switching to and self-updating to pnpm v12. pnpm v12 (the Rust port) ships as the pnpm and @pnpm/exe npm packages whose bins are placeholders replaced at install time by the host's native binary from a @pnpm/exe.<platform>-<arch>[-musl] optional dependency. Because pnpm installs its own engine with --ignore-scripts, that relinking never ran, leaving a non-executable placeholder. pnpm now relinks the native binary itself for v12 (recognizing the new platform-package naming scheme and the native pnpm package), and verifies the native binary's npm registry signature before running it.
36928be: ${...} environment-variable placeholders in the httpProxy, httpsProxy, noProxy, proxy, and noproxy settings are no longer expanded when these settings come from a project's pnpm-workspace.yaml. They now receive the same protection already applied to registry.
352ae48: Security: validate config dependency names and versions before using them to build filesystem paths. A pnpm-workspace.yaml with a traversal-shaped configDependencies name (such as ../../PWNED) or version (such as ../../../PWNED) could previously cause pnpm install to create symlinks or write package files outside node_modules/.pnpm-config and the store. Names must now be valid npm package names and versions must be exact semver versions. See GHSA-qrv3-253h-g69c.
352ae48: Reject path-traversal and reserved dependency aliases (such as ../../../escape, .bin, .pnpm, or node_modules) that come from a lockfile rather than a freshly resolved manifest. A crafted lockfile alias could otherwise be joined directly under a hoisted node_modules directory, letting package files be written outside the intended install root or overwrite pnpm-owned layout.
The nodeLinker: hoisted graph builder now validates each alias at the directory sink (safeJoinModulesDir), matching the validation pnpm already performs when resolving aliases from manifests. See GHSA-fr4h-3cph-29xv.
352ae48: Prevent pnpm patch-remove from removing files outside the configured patches directory.
217fbe0: Hardened the warning printed when a project .npmrc uses environment variables in registry/auth settings: the suggested pnpm config set command is now only included for keys made up of shell-inert characters. Because the key comes from a repository-controlled .npmrc and a shell expands $(...), backticks, and $VAR even inside double quotes, a crafted key could otherwise have turned the suggested copy-paste command into command execution.
⚠️ Security fix — environment variables in a project .npmrc (action may be required)
Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands ${ENV_VAR} placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
This release also closes a bypass where a project .npmrc could set userconfig, globalconfig, or prefix to make pnpm load a repo-supplied file as trusted config (via @pnpm/npm-conf@3.0.3).
Environment variables are still expanded in trusted config: your user-level ~/.npmrc, the global config, CLI options, and environment config.
If your authentication broke after upgrading, move the token out of the committed .npmrc:
# Writes to your user/global config, not the repository:
pnpm config set"//registry.npmjs.org/:_authToken""$NPM_TOKEN"
Or keep the ${NPM_TOKEN} line but put it in your user-level ~/.npmrc instead of the repo. In GitHub Actions, actions/setup-node with registry-url already writes a user-level .npmrc, so NODE_AUTH_TOKEN keeps working. For other CI where editing each pipeline is hard, set NPM_CONFIG_USERCONFIG=.npmrc in the CI environment to declare the project .npmrc trusted.
Improved the warning printed when a project .npmrc uses an environment variable in a registry/proxy URL or in registry credentials. The message now explains why the setting was ignored and how to migrate it to a trusted source — for example by running pnpm config set "<key>" <value> to store it in the global config, or by keeping the ${...} line in the user-level ~/.npmrc — with a link to https://pnpm.io/npmrc.
A repository-controlled project or workspace .npmrc can no longer redirect which files pnpm loads as its trusted user and global configuration. Previously such a file could set userconfig, globalconfig, or prefix to point at an attacker-supplied file shipped in the repository, and pnpm would load it as a trusted config source — bypassing the protection that prevents repository config from expanding environment variables into registry request destinations and credentials, and allowing it to set tokenHelper. The user/global config file locations are now resolved only from trusted sources (CLI options, environment config, the npm builtin config, and defaults) before the project and workspace .npmrc files are read. Fixed by upgrading @pnpm/npm-conf to 3.0.3.
⚠️ Security fix — environment variables in a project .npmrc (action may be required)
Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands ${ENV_VAR} placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
This release also closes a bypass where a project .npmrc could set userconfig, globalconfig, or prefix to make pnpm load a repo-supplied file as trusted config (via @pnpm/npm-conf@3.0.3).
Environment variables are still expanded in trusted config: your user-level ~/.npmrc, the global config, CLI options, and environment config.
If your authentication broke after upgrading, move the token out of the committed .npmrc:
# Writes to your user/global config, not the repository:
pnpm config set"//registry.npmjs.org/:_authToken""$NPM_TOKEN"
Or keep the ${NPM_TOKEN} line but put it in your user-level ~/.npmrc instead of the repo. In GitHub Actions, actions/setup-node with registry-url already writes a user-level .npmrc, so NODE_AUTH_TOKEN keeps working. For other CI where editing each pipeline is hard, set NPM_CONFIG_USERCONFIG=.npmrc in the CI environment to declare the project .npmrc trusted.
Package-manager bootstrap traffic is now resolved through trusted registries and trusted network config. When pnpm downloads the pnpm version requested by a repository's packageManager field, the registry it fetches from (and the proxy/TLS settings used for that traffic) now come exclusively from trusted config sources — CLI options, env config, user and global .npmrc — defaulting to the public npm registry, instead of the repository's project/workspace settings.
pnpm now verifies the npm registry signature of a package-manager binary before spawning it. When the packageManager field (or pnpm self-update) makes pnpm download another pnpm version, the staged install is verified corepack-style: the integrity recorded in the staged lockfile must carry a valid npm registry signature for the exact name@version, validated against npm's public signing keys that ship embedded in the pnpm CLI. Verification fails closed — a tampered download, an unsigned package, or an unreachable registry refuses the version switch rather than running an unverified binary. It runs only when the wanted version is actually downloaded (a tools-directory cache miss), so repeated commands pay no extra network round trip.
Environment variable expansion is now trust-aware for registry/auth config and request destinations. Repository-controlled config files (the project and workspace .npmrc and pnpm-workspace.yaml) can no longer expand ${...} placeholders in registry/proxy request destinations, URL-scoped keys, or registry credential values, preventing repository-controlled configuration from exfiltrating environment secrets through request URLs. Trusted user/global/CLI/env config keeps full env expansion, so existing token and registry setup flows continue to work.
Reject reserved manifest bin names ("", ".", "..", and scoped forms such as @scope/..) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted.
Require trusted package identity before package-name onlyBuiltDependencies (and allowBuilds) entries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as the key. Lockfile entries are now rejected when a registry-style dependency path (name@semver) is backed by a git, directory, or git-hosted tarball resolution (ERR_PNPM_RESOLUTION_SHAPE_MISMATCH), so the dependency path is a reliable artifact identity by the time scripts can run.
pnpm now verifies the detached OpenPGP signature of a Node.js release's SHASUMS256.txt against the Node.js release team's public keys (embedded in the pnpm CLI) before trusting its hashes. The Node.js download mirror is repository-configurable (node-mirror:<channel> in .npmrc), and the integrity check previously trusted a SHASUMS256.txt fetched from that same mirror — a circular check that a malicious mirror could satisfy with a tampered binary and matching hashes. A mirror that proxies the real signed SHASUMS keeps working unchanged. Only the release channel publishes signed SHASUMS files, so pre-release channels (rc, nightly, …) remain unverified.
Reject pnpm-lock.yaml entries whose remote tarball resolution: block is missing the integrity field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips integrity:) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under --frozen-lockfile. pnpm now fails closed at lockfile-read time with ERR_PNPM_MISSING_TARBALL_INTEGRITY. Git-hosted tarballs (gitHosted: true or a URL on codeload.github.com / bitbucket.org / gitlab.com) and file: tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.
Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, pnpm install (non-frozen) would log ERR_PNPM_TARBALL_INTEGRITY, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.
pnpm install now exits with ERR_PNPM_TARBALL_INTEGRITY and a hint pointing at the new opt-in flag.
The only opt-in is pnpm install --update-checksums — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.
--force and pnpm update deliberately do not bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. --frozen-lockfile behavior is unchanged. --fix-lockfile keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass.
Patch Changes
Pin unscoped per-registry settings (_authToken, _auth, username/_password, tokenHelper, inline cert/key) to the registry declared in the same config source at load time, so a later layer overriding registry= (workspace .npmrc, pnpm-workspace.yaml, CLI --registry) cannot redirect a credential or client certificate authored for a different host. A deprecation warning is emitted whenever an unscoped per-registry setting is encountered, naming the source and the URL it was pinned to. Reported by JUNYI LIU.
Fixed minimumReleaseAge handling when cached metadata is abbreviated. The npm registry returns abbreviated package metadata (without the per-version time field) by default, which made the maturity check throw ERR_PNPM_MISSING_TIME whenever cached abbreviated metadata was reused. pnpm now upgrades cached abbreviated metadata to the full document via a follow-up fetch when minimumReleaseAge is active, persists the upgrade to the on-disk cache so subsequent installs skip the extra fetch, and lets ERR_PNPM_MISSING_TIME from the cache fast-path fall through to the network fetch even under strict mode.
Reject git resolutions whose commit field is not a 40-character hexadecimal SHA before invoking git. A malicious lockfile could otherwise smuggle a value such as --upload-pack=<command> through git fetch / git checkout, which on SSH or local-file transports executes the supplied command.
Reject patch files whose diff --git headers reference paths outside the patched package directory. Previously a malicious .patch file added via a pull request could write, delete, or rename arbitrary files reachable by the user running pnpm install.
Fixed --prefix=<dir> not being honored when locating the workspace root. The --prefix → dir rename was applied after workspace detection, so workspace settings declared in <dir>/pnpm-workspace.yaml were not loaded when pnpm was invoked from outside <dir>#11535.
Reject dependency aliases that contain path-traversal segments (such as @x/../../../../../.git/hooks) when reading them from a package manifest or symlinking them into node_modules. A malicious registry package could otherwise use a transitive dependency key to make pnpm install create symlinks at attacker-chosen paths outside the intended node_modules directory.
Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
A new gitHosted: true field is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.
Fix a regression where pnpm --recursive --filter '!<pkg>' run/exec/test/add would include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative --filter arguments are provided, matching the documented behavior. To include the root, pass --include-workspace-root#11341.
When self-updating from v10's @pnpm/exe to v11+ on Intel macOS (darwin-x64), pnpm self-update now transparently switches to the JS-only pnpm package on npm instead of installing @pnpm/exe@v11+ (which doesn't ship a working binary for Intel Macs because of an upstream Node.js SEA bug — see #11423 and nodejs/node#62893). Without this, the self-update would silently leave the user with no working pnpm binary. The new install requires Node.js to be available on PATH; a warning is printed when the swap happens. All other host/version combinations are unchanged.
pnpm self-update (with no version argument) no longer downgrades pnpm when the registry's latest dist-tag points to an older release than the currently active version. Run pnpm self-update latest to force a downgrade #11418.
Globally-installed bins no longer fail with ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND when pnpm was installed via the standalone @pnpm/exe binary (e.g. curl -fsSL https://get.pnpm.io/install.sh | sh -) on a system without a separate Node.js installation. Previously, when which('node') failed during pnpm add --global, pnpm fell back to process.execPath, which in @pnpm/exe is the pnpm binary itself — and that path was baked into the generated bin shim, causing the shim to invoke pnpm instead of Node #11291, #4645.
Fix an infinite fork-bomb that could happen when pnpm was installed with one version (e.g. npm install -g pnpm@A) and run inside a project whose package.json selected a different pnpm version via the packageManager field (e.g. pnpm@B), while a pnpm-workspace.yaml also existed at the project root.
The child's environment is now forced to manage-package-manager-versions=false (v10) and pm-on-fail=ignore (v11+), which disables the package-manager-version handling in whichever pnpm runs as the child.
When a project's packageManager field selects pnpm v11 or newer, commands that v10 would have passed through to npm (version, login, logout, publish, unpublish, deprecate, dist-tag, docs, ping, search, star, stars, unstar, whoami, etc.) are now handed over to the wanted pnpm, which implements them natively. Previously they silently shelled out to npm — making, for example, pnpm version --help print npm's help on a project with packageManager: pnpm@11.0.0-rc.3#11328.
> ℹ️ **Note**
>
> This PR body was truncated due to platform limits.
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node) ([source](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)) | devDependencies | major | [`^25.0.0` → `^26.0.0`](https://renovatebot.com/diffs/npm/@types%2fnode/25.5.0/26.6.3) |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme) ([source](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react)) | devDependencies | major | [`^5.0.0` → `^6.0.0`](https://renovatebot.com/diffs/npm/@vitejs%2fplugin-react/5.2.0/6.1.1) |
| [actions/checkout](https://github.com/actions/checkout) | action | major | `v6` → `v7` |
| [docker.io/library/node](https://hub.docker.com/_/node) ([source](https://github.com/nodejs/docker-node)) | stage | major | `25` → `26` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | action | major | `v6` → `v7` |
| [docker/login-action](https://github.com/docker/login-action) | action | major | `v3` → `v4` |
| [docker/metadata-action](https://github.com/docker/metadata-action) | action | major | `v5` → `v6` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | action | major | `v3` → `v4` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | action | major | `v3` → `v4` |
| [jsdom](https://github.com/jsdom/jsdom) | devDependencies | major | [`^28.0.0` → `^30.0.0`](https://renovatebot.com/diffs/npm/jsdom/28.1.0/30.1.1) |
| [pnpm](https://github.com/pnpm/pnpm/tree/main/pnpm) ([source](https://github.com/pnpm/pnpm/tree/HEAD/pnpm/npm/pnpm)) | packageManager | major | [`10.33.0` → `12.6.0`](https://renovatebot.com/diffs/npm/pnpm/10.33.0/12.6.0) |
| [typescript](https://www.typescriptlang.org/) ([source](https://github.com/microsoft/TypeScript)) | devDependencies | major | [`^5.7.3` → `^7.0.0`](https://renovatebot.com/diffs/npm/typescript/5.9.3/7.0.2) |
| [vitest](https://vitest.dev) ([source](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | devDependencies | major | [`^4.0.0` → `^5.0.0`](https://renovatebot.com/diffs/npm/vitest/4.1.2/5.0.2) |
---
### Release Notes
<details>
<summary>vitejs/vite-plugin-react (@​vitejs/plugin-react)</summary>
### [`v6.1.1`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#611-2026-08-28)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/39b31735bf79c2dd380eedaba7ed849256f92a29...04cac5020e349f452d76c5a4f6d788ad4b38930a)
##### Add `compiler.logDiagnostics` option
Recoverable React Compiler diagnostics are no longer logged by default. Set `compiler.logDiagnostics` to `true` to log them through Vite. Fatal diagnostics are always logged and fail the transform.
##### Respect environment sourcemap option for React Compiler transform when `builder.sharedPlugins` is enabled ([#​1439](https://github.com/vitejs/vite-plugin-react/pull/1439))
The React Compiler transform was using the top-level sourcemap option instead of the environment sourcemap option. This caused a problem when the experimental `builder.sharedPlugins` was enabled.
### [`v6.1.0`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#610-2026-08-19)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/68c0cb8796ce18bd049c3d05c5210eaf0617eac0...39b31735bf79c2dd380eedaba7ed849256f92a29)
##### Add experimental native React Compiler support ([#​1419](https://github.com/vitejs/vite-plugin-react/pull/1419))
Add experimental native React Compiler support.
You can use it by installing `oxc-transform-react` and enabling it via the `compiler` option:
```sh
npm install -D oxc-transform-react
```
```js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react({ compiler: true })
]
})
```
### [`v6.0.5`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#605-2026-07-30)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/f4b549822ec239799d746c030abb0b9a7d8f0a04...68c0cb8796ce18bd049c3d05c5210eaf0617eac0)
##### Fixed the react compiler preset filter to be linear ([#​1353](https://github.com/vitejs/vite-plugin-react/pull/1353))
The improved filter in v6.0.3 was non-linear and caused a performance regression ([#​1349](https://github.com/vitejs/vite-plugin-react/issues/1349)). The filter was changed to be linear to avoid that.
### [`v6.0.4`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#604-2026-07-22)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/640fd358a0e82393acfce4e92e19a6ac6e1641a7...f4b549822ec239799d746c030abb0b9a7d8f0a04)
##### Fixed `$RefreshSig$ is not defined` error when running `vite dev` with `NODE_ENV=production`
When running `vite dev` with `NODE_ENV=production`, the app errored with `$RefreshSig$ is not defined`.
This error is now fixed.
### [`v6.0.3`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#603-2026-06-23)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/6535b55e956b425e6650ffc2cc98fd23cca1d231...640fd358a0e82393acfce4e92e19a6ac6e1641a7)
##### Improve the react compiler preset filter to reduce false-positives ([#​1138](https://github.com/vitejs/vite-plugin-react/pull/1138))
Improved the filter in the react compiler babel preset to reduce the false-positives so that less modules are processed by the react compiler.
### [`v6.0.2`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#602-2026-05-14)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/1e94c06995c2afe2d1fee5aea2ef9720d35a7e02...6535b55e956b425e6650ffc2cc98fd23cca1d231)
##### Allow all options in reactCompilerPreset ([#​1189](https://github.com/vitejs/vite-plugin-react/pull/1189))
This is a type only change. Only `compilationMode` and `target` options were available for `reactCompilerPreset`.
### [`v6.0.1`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#601-2026-03-13)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/dcc901236079ef7fa99139f7ba7beebac583f301...1e94c06995c2afe2d1fee5aea2ef9720d35a7e02)
##### Expand `@rolldown/plugin-babel` peer dep range ([#​1146](https://github.com/vitejs/vite-plugin-react/pull/1146))
Expanded `@rolldown/plugin-babel` peer dep range to include `^0.2.0`.
### [`v6.0.0`](https://github.com/vitejs/vite-plugin-react/blob/HEAD/packages/plugin-react/CHANGELOG.md#600-2026-03-12)
[Compare Source](https://github.com/vitejs/vite-plugin-react/compare/fda3a86095556b49ae3c995eb57a30d4e0b8fa8d...dcc901236079ef7fa99139f7ba7beebac583f301)
</details>
<details>
<summary>actions/checkout (actions/checkout)</summary>
### [`v7.0.1`](https://github.com/actions/checkout/blob/HEAD/CHANGELOG.md#v701)
[Compare Source](https://github.com/actions/checkout/compare/v7.0.0...v7.0.1)
- Skip running unsafe pr check if input is default by [@​aiqiaoy](https://github.com/aiqiaoy) in [#​2518](https://github.com/actions/checkout/pull/2518)
- Trim only ascii whitespace for branch by [@​aiqiaoy](https://github.com/aiqiaoy) in [#​2521](https://github.com/actions/checkout/pull/2521)
- Escape values passed to --unset by [@​aiqiaoy](https://github.com/aiqiaoy) in [#​2530](https://github.com/actions/checkout/pull/2530)
- Various dependency updates
### [`v7.0.0`](https://github.com/actions/checkout/blob/HEAD/CHANGELOG.md#v700)
[Compare Source](https://github.com/actions/checkout/compare/v6.1.0...v7.0.0)
- Block checking out fork PR for pull\_request\_target and workflow\_run by [@​aiqiaoy](https://github.com/aiqiaoy) in [#​2454](https://github.com/actions/checkout/pull/2454)
- Various dependency updates
</details>
<details>
<summary>docker/build-push-action (docker/build-push-action)</summary>
### [`v7.4.0`](https://github.com/docker/build-push-action/releases/tag/v7.4.0)
[Compare Source](https://github.com/docker/build-push-action/compare/v7.3.0...v7.4.0)
- Use the shared error helper for Buildx commands by [@​crazy-max](https://github.com/crazy-max) in [#​1620](https://github.com/docker/build-push-action/pull/1620)
- Prevent workflow command injection in metadata logs by [@​crazy-max](https://github.com/crazy-max) in [#​1617](https://github.com/docker/build-push-action/pull/1617)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.92.0 to 0.100.0 in [#​1614](https://github.com/docker/build-push-action/pull/1614) [#​1618](https://github.com/docker/build-push-action/pull/1618) [#​1621](https://github.com/docker/build-push-action/pull/1621)
- Bump [@​humanfs/node](https://github.com/humanfs/node) from 0.16.7 to 0.16.8 in [#​1609](https://github.com/docker/build-push-action/pull/1609)
- Bump brace-expansion from 1.1.13 to 1.1.18 in [#​1592](https://github.com/docker/build-push-action/pull/1592)
- Bump csv-parse from 7.0.0 to 7.0.2 in [#​1613](https://github.com/docker/build-push-action/pull/1613)
- Bump js-yaml from 4.3.0 to 4.3.2 in [#​1605](https://github.com/docker/build-push-action/pull/1605) [#​1615](https://github.com/docker/build-push-action/pull/1615)
- Bump nanoid from 3.3.16 to 3.3.18 in [#​1611](https://github.com/docker/build-push-action/pull/1611)
- Bump postcss from 8.5.10 to 8.5.25 in [#​1590](https://github.com/docker/build-push-action/pull/1590)
- Bump postcss-selector-parser from 7.1.1 to 7.1.5 in [#​1606](https://github.com/docker/build-push-action/pull/1606)
- Bump sigstore from 4.1.0 to 4.1.1 in [#​1577](https://github.com/docker/build-push-action/pull/1577)
- Bump undici from 6.27.0 to 6.28.0 in [#​1594](https://github.com/docker/build-push-action/pull/1594)
**Full Changelog**: <https://github.com/docker/build-push-action/compare/v7.3.0...v7.4.0>
### [`v7.3.0`](https://github.com/docker/build-push-action/releases/tag/v7.3.0)
[Compare Source](https://github.com/docker/build-push-action/compare/v7.2.0...v7.3.0)
- Preserve names in esbuild bundle by [@​crazy-max](https://github.com/crazy-max) in [#​1567](https://github.com/docker/build-push-action/pull/1567)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.90.0 to 0.92.0 in [#​1545](https://github.com/docker/build-push-action/pull/1545) [#​1572](https://github.com/docker/build-push-action/pull/1572)
- Bump [@​sigstore/core](https://github.com/sigstore/core) from 3.1.0 to 3.2.1 in [#​1568](https://github.com/docker/build-push-action/pull/1568)
- Bump js-yaml from 4.1.1 to 4.3.0 in [#​1566](https://github.com/docker/build-push-action/pull/1566)
- Bump tmp from 0.2.5 to 0.2.7 in [#​1547](https://github.com/docker/build-push-action/pull/1547)
- Bump undici from 6.24.1 to 6.27.0 in [#​1564](https://github.com/docker/build-push-action/pull/1564)
- Bump vite from 7.3.2 to 7.3.6 in [#​1563](https://github.com/docker/build-push-action/pull/1563)
**Full Changelog**: <https://github.com/docker/build-push-action/compare/v7.2.0...v7.3.0>
### [`v7.2.0`](https://github.com/docker/build-push-action/releases/tag/v7.2.0)
[Compare Source](https://github.com/docker/build-push-action/compare/v7.1.0...v7.2.0)
- Bump [@​actions/core](https://github.com/actions/core) from 3.0.0 to 3.0.1 in [#​1525](https://github.com/docker/build-push-action/pull/1525)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.87.0 to 0.90.0 in [#​1517](https://github.com/docker/build-push-action/pull/1517)
- Bump brace-expansion from 2.0.2 to 5.0.6 in [#​1534](https://github.com/docker/build-push-action/pull/1534)
- Bump fast-xml-builder from 1.1.4 to 1.2.0 in [#​1529](https://github.com/docker/build-push-action/pull/1529)
- Bump fast-xml-parser from 5.5.7 to 5.8.0 in [#​1521](https://github.com/docker/build-push-action/pull/1521)
- Bump postcss from 8.5.6 to 8.5.10 in [#​1526](https://github.com/docker/build-push-action/pull/1526)
- Bump tar from 6.2.1 to 7.5.15 in [#​1533](https://github.com/docker/build-push-action/pull/1533)
**Full Changelog**: <https://github.com/docker/build-push-action/compare/v7.1.0...v7.2.0>
### [`v7.1.0`](https://github.com/docker/build-push-action/releases/tag/v7.1.0)
[Compare Source](https://github.com/docker/build-push-action/compare/v7.0.0...v7.1.0)
- Git context [query format](https://docs.docker.com/build/concepts/context/#url-queries) support by [@​crazy-max](https://github.com/crazy-max) in [#​1505](https://github.com/docker/build-push-action/pull/1505)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.79.0 to 0.87.0 by [@​crazy-max](https://github.com/crazy-max) in [#​1505](https://github.com/docker/build-push-action/pull/1505)
- Bump brace-expansion from 1.1.12 to 1.1.13 in [#​1500](https://github.com/docker/build-push-action/pull/1500)
- Bump fast-xml-parser from 5.4.2 to 5.5.7 in [#​1489](https://github.com/docker/build-push-action/pull/1489)
- Bump flatted from 3.3.3 to 3.4.2 in [#​1491](https://github.com/docker/build-push-action/pull/1491)
- Bump glob from 10.3.12 to 10.5.0 in [#​1490](https://github.com/docker/build-push-action/pull/1490)
- Bump handlebars from 4.7.8 to 4.7.9 in [#​1497](https://github.com/docker/build-push-action/pull/1497)
- Bump lodash from 4.17.23 to 4.18.1 in [#​1510](https://github.com/docker/build-push-action/pull/1510)
- Bump picomatch from 4.0.3 to 4.0.4 in [#​1496](https://github.com/docker/build-push-action/pull/1496)
- Bump undici from 6.23.0 to 6.24.1 in [#​1486](https://github.com/docker/build-push-action/pull/1486)
- Bump vite from 7.3.1 to 7.3.2 in [#​1509](https://github.com/docker/build-push-action/pull/1509)
**Full Changelog**: <https://github.com/docker/build-push-action/compare/v7.0.0...v7.1.0>
### [`v7.0.0`](https://github.com/docker/build-push-action/releases/tag/v7.0.0)
[Compare Source](https://github.com/docker/build-push-action/compare/v6.19.2...v7.0.0)
- Node 24 as default runtime (requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later) by [@​crazy-max](https://github.com/crazy-max) in [#​1470](https://github.com/docker/build-push-action/pull/1470)
- Remove deprecated `DOCKER_BUILD_NO_SUMMARY` and `DOCKER_BUILD_EXPORT_RETENTION_DAYS` envs by [@​crazy-max](https://github.com/crazy-max) in [#​1473](https://github.com/docker/build-push-action/pull/1473)
- Remove legacy export-build tool support for build summary by [@​crazy-max](https://github.com/crazy-max) in [#​1474](https://github.com/docker/build-push-action/pull/1474)
- Switch to ESM and update config/test wiring by [@​crazy-max](https://github.com/crazy-max) in [#​1466](https://github.com/docker/build-push-action/pull/1466)
- Bump [@​actions/core](https://github.com/actions/core) from 1.11.1 to 3.0.0 in [#​1454](https://github.com/docker/build-push-action/pull/1454)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.62.1 to 0.79.0 in [#​1453](https://github.com/docker/build-push-action/pull/1453) [#​1472](https://github.com/docker/build-push-action/pull/1472) [#​1479](https://github.com/docker/build-push-action/pull/1479)
- Bump minimatch from 3.1.2 to 3.1.5 in [#​1463](https://github.com/docker/build-push-action/pull/1463)
**Full Changelog**: <https://github.com/docker/build-push-action/compare/v6.19.2...v7.0.0>
</details>
<details>
<summary>docker/login-action (docker/login-action)</summary>
### [`v4.6.0`](https://github.com/docker/login-action/releases/tag/v4.6.0)
[Compare Source](https://github.com/docker/login-action/compare/v4.5.2...v4.6.0)
- Harden buildx scoped config path handling by [@​crazy-max](https://github.com/crazy-max) in [#​1059](https://github.com/docker/login-action/pull/1059)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) to 3.1095.0 in [#​1051](https://github.com/docker/login-action/pull/1051)
- Bump js-yaml from 5.2.1 to 5.2.2 in [#​1057](https://github.com/docker/login-action/pull/1057)
- Bump postcss from 8.5.10 to 8.5.22 in [#​1056](https://github.com/docker/login-action/pull/1056)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.5.2...v4.6.0>
### [`v4.5.2`](https://github.com/docker/login-action/releases/tag/v4.5.2)
[Compare Source](https://github.com/docker/login-action/compare/v4.5.1...v4.5.2)
- Surface Docker Hub OIDC error responses by [@​crazy-max](https://github.com/crazy-max) in [#​1058](https://github.com/docker/login-action/pull/1058)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.5.1...v4.5.2>
### [`v4.5.1`](https://github.com/docker/login-action/releases/tag/v4.5.1)
[Compare Source](https://github.com/docker/login-action/compare/v4.5.0...v4.5.1)
- Support `dhi.io` as Docker Hub OIDC registry by [@​crazy-max](https://github.com/crazy-max) in [#​1054](https://github.com/docker/login-action/pull/1054)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.5.0...v4.5.1>
### [`v4.5.0`](https://github.com/docker/login-action/releases/tag/v4.5.0)
[Compare Source](https://github.com/docker/login-action/compare/v4.4.0...v4.5.0)
- [Docker Hub OIDC](https://github.com/docker/login-action#docker-hub) login support by [@​crazy-max](https://github.com/crazy-max) in [#​1048](https://github.com/docker/login-action/pull/1048)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) to 3.1091.0 in [#​1037](https://github.com/docker/login-action/pull/1037)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.92.0 to 0.94.0 in [#​1044](https://github.com/docker/login-action/pull/1044) [#​1050](https://github.com/docker/login-action/pull/1050)
- Bump brace-expansion from 1.1.13 to 1.1.16 in [#​1046](https://github.com/docker/login-action/pull/1046)
- Bump js-yaml from 5.2.0 to 5.2.1 in [#​1038](https://github.com/docker/login-action/pull/1038)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.4.0...v4.5.0>
### [`v4.4.0`](https://github.com/docker/login-action/releases/tag/v4.4.0)
[Compare Source](https://github.com/docker/login-action/compare/v4.3.0...v4.4.0)
- Skip empty `registry-auth` secret mask by [@​crazy-max](https://github.com/crazy-max) in [#​1035](https://github.com/docker/login-action/pull/1035)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) to 3.1077.0 [#​1034](https://github.com/docker/login-action/pull/1034)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.3.0...v4.4.0>
### [`v4.3.0`](https://github.com/docker/login-action/releases/tag/v4.3.0)
[Compare Source](https://github.com/docker/login-action/compare/v4.2.0...v4.3.0)
- Preserve names in esbuild bundle by [@​crazy-max](https://github.com/crazy-max) in [#​1022](https://github.com/docker/login-action/pull/1022)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) to 3.1076.0 [#​999](https://github.com/docker/login-action/pull/999) [#​1030](https://github.com/docker/login-action/pull/1030)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.90.0 to 0.92.0 in [#​1004](https://github.com/docker/login-action/pull/1004) [#​1027](https://github.com/docker/login-action/pull/1027)
- Bump [@​sigstore/core](https://github.com/sigstore/core) from 3.1.0 to 3.2.1 in [#​1023](https://github.com/docker/login-action/pull/1023)
- Bump [@​sigstore/verify](https://github.com/sigstore/verify) from 3.1.0 to 3.1.1 in [#​1029](https://github.com/docker/login-action/pull/1029)
- Bump http-proxy-agent and https-proxy-agent to 9.1.0 in [#​1017](https://github.com/docker/login-action/pull/1017)
- Bump js-yaml from 4.1.1 to 5.2.0 in [#​1028](https://github.com/docker/login-action/pull/1028)
- Bump sigstore from 4.1.0 to 4.1.1 in [#​1031](https://github.com/docker/login-action/pull/1031)
- Bump tmp from 0.2.5 to 0.2.7 in [#​1002](https://github.com/docker/login-action/pull/1002)
- Bump undici from 6.24.1 to 6.27.0 in [#​1020](https://github.com/docker/login-action/pull/1020)
- Bump vite from 7.3.3 to 7.3.6 in [#​1019](https://github.com/docker/login-action/pull/1019)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.2.0...v4.3.0>
### [`v4.2.0`](https://github.com/docker/login-action/releases/tag/v4.2.0)
[Compare Source](https://github.com/docker/login-action/compare/v4.1.0...v4.2.0)
- Bump [@​actions/core](https://github.com/actions/core) from 3.0.0 to 3.0.1 in [#​976](https://github.com/docker/login-action/pull/976)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) to 3.1050.0 in [#​960](https://github.com/docker/login-action/pull/960)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.86.0 to 0.90.0 in [#​970](https://github.com/docker/login-action/pull/970)
- Bump brace-expansion from 2.0.1 to 5.0.6 in [#​993](https://github.com/docker/login-action/pull/993)
- Bump fast-xml-builder from 1.1.4 to 1.2.0 in [#​985](https://github.com/docker/login-action/pull/985)
- Bump fast-xml-parser from 5.3.6 to 5.8.0 in [#​963](https://github.com/docker/login-action/pull/963)
- Bump http-proxy-agent and https-proxy-agent to 9.0.0 in [#​961](https://github.com/docker/login-action/pull/961)
- Bump postcss from 8.5.6 to 8.5.10 in [#​979](https://github.com/docker/login-action/pull/979)
- Bump tar from 6.2.1 to 7.5.15 in [#​991](https://github.com/docker/login-action/pull/991)
- Bump vite from 7.3.1 to 7.3.3 in [#​986](https://github.com/docker/login-action/pull/986)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.1.0...v4.2.0>
### [`v4.1.0`](https://github.com/docker/login-action/releases/tag/v4.1.0)
[Compare Source](https://github.com/docker/login-action/compare/v4.0.0...v4.1.0)
- Fix scoped Docker Hub cleanup path when registry is omitted by [@​crazy-max](https://github.com/crazy-max) in [#​945](https://github.com/docker/login-action/pull/945)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) to 3.1020.0 in [#​930](https://github.com/docker/login-action/pull/930)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.77.0 to 0.86.0 in [#​932](https://github.com/docker/login-action/pull/932) [#​936](https://github.com/docker/login-action/pull/936)
- Bump brace-expansion from 1.1.12 to 1.1.13 in [#​952](https://github.com/docker/login-action/pull/952)
- Bump fast-xml-parser from 5.3.4 to 5.3.6 in [#​942](https://github.com/docker/login-action/pull/942)
- Bump flatted from 3.3.3 to 3.4.2 in [#​944](https://github.com/docker/login-action/pull/944)
- Bump glob from 10.3.12 to 10.5.0 in [#​940](https://github.com/docker/login-action/pull/940)
- Bump handlebars from 4.7.8 to 4.7.9 in [#​949](https://github.com/docker/login-action/pull/949)
- Bump http-proxy-agent and https-proxy-agent to 8.0.0 in [#​937](https://github.com/docker/login-action/pull/937)
- Bump lodash from 4.17.23 to 4.18.1 in [#​958](https://github.com/docker/login-action/pull/958)
- Bump minimatch from 3.1.2 to 3.1.5 in [#​941](https://github.com/docker/login-action/pull/941)
- Bump picomatch from 4.0.3 to 4.0.4 in [#​948](https://github.com/docker/login-action/pull/948)
- Bump undici from 6.23.0 to 6.24.1 in [#​938](https://github.com/docker/login-action/pull/938)
**Full Changelog**: <https://github.com/docker/login-action/compare/v4.0.0...v4.1.0>
### [`v4.0.0`](https://github.com/docker/login-action/releases/tag/v4.0.0)
[Compare Source](https://github.com/docker/login-action/compare/v3.7.0...v4.0.0)
- Node 24 as default runtime (requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later) by [@​crazy-max](https://github.com/crazy-max) in [#​929](https://github.com/docker/login-action/pull/929)
- Switch to ESM and update config/test wiring by [@​crazy-max](https://github.com/crazy-max) in [#​927](https://github.com/docker/login-action/pull/927)
- Bump [@​actions/core](https://github.com/actions/core) from 1.11.1 to 3.0.0 in [#​919](https://github.com/docker/login-action/pull/919)
- Bump [@​aws-sdk/client-ecr](https://github.com/aws-sdk/client-ecr) from 3.890.0 to 3.1000.0 in [#​909](https://github.com/docker/login-action/pull/909) [#​920](https://github.com/docker/login-action/pull/920)
- Bump [@​aws-sdk/client-ecr-public](https://github.com/aws-sdk/client-ecr-public) from 3.890.0 to 3.1000.0 in [#​909](https://github.com/docker/login-action/pull/909) [#​920](https://github.com/docker/login-action/pull/920)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.63.0 to 0.77.0 in [#​910](https://github.com/docker/login-action/pull/910) [#​928](https://github.com/docker/login-action/pull/928)
- Bump [@​isaacs/brace-expansion](https://github.com/isaacs/brace-expansion) from 5.0.0 to 5.0.1 in [#​921](https://github.com/docker/login-action/pull/921)
- Bump js-yaml from 4.1.0 to 4.1.1 in [#​901](https://github.com/docker/login-action/pull/901)
**Full Changelog**: <https://github.com/docker/login-action/compare/v3.7.0...v4.0.0>
</details>
<details>
<summary>docker/metadata-action (docker/metadata-action)</summary>
### [`v6.2.0`](https://github.com/docker/metadata-action/releases/tag/v6.2.0)
[Compare Source](https://github.com/docker/metadata-action/compare/v6.1.0...v6.2.0)
- Preserve names in esbuild bundle by [@​crazy-max](https://github.com/crazy-max) in [#​689](https://github.com/docker/metadata-action/pull/689)
- Bump [@​actions/core](https://github.com/actions/core) from 3.0.0 to 3.0.1 in [#​663](https://github.com/docker/metadata-action/pull/663)
- Bump [@​actions/github](https://github.com/actions/github) from 9.0.0 to 9.1.1 in [#​666](https://github.com/docker/metadata-action/pull/666)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.90.0 to 0.92.0 in [#​672](https://github.com/docker/metadata-action/pull/672) [#​696](https://github.com/docker/metadata-action/pull/696)
- Bump [@​sigstore/core](https://github.com/sigstore/core) from 3.1.0 to 3.2.1 in [#​690](https://github.com/docker/metadata-action/pull/690)
- Bump [@​sigstore/verify](https://github.com/sigstore/verify) from 3.1.0 to 3.1.1 in [#​693](https://github.com/docker/metadata-action/pull/693)
- Bump csv-parse from 6.2.1 to 7.0.0 in [#​683](https://github.com/docker/metadata-action/pull/683)
- Bump js-yaml from 4.1.1 to 4.3.0 in [#​688](https://github.com/docker/metadata-action/pull/688)
- Bump moment-timezone from 0.6.1 to 0.6.2 in [#​664](https://github.com/docker/metadata-action/pull/664)
- Bump semver from 7.7.4 to 7.8.5 in [#​665](https://github.com/docker/metadata-action/pull/665) [#​695](https://github.com/docker/metadata-action/pull/695)
- Bump sigstore from 4.1.0 to 4.1.1 in [#​694](https://github.com/docker/metadata-action/pull/694)
- Bump tmp from 0.2.5 to 0.2.7 in [#​673](https://github.com/docker/metadata-action/pull/673)
- Bump undici from 6.25.0 to 6.27.0 in [#​686](https://github.com/docker/metadata-action/pull/686)
- Bump vite from 7.3.2 to 7.3.6 in [#​685](https://github.com/docker/metadata-action/pull/685)
**Full Changelog**: <https://github.com/docker/metadata-action/compare/v6.1.0...v6.2.0>
### [`v6.1.0`](https://github.com/docker/metadata-action/releases/tag/v6.1.0)
[Compare Source](https://github.com/docker/metadata-action/compare/v6.0.0...v6.1.0)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.79.0 to 0.90.0 in [#​613](https://github.com/docker/metadata-action/pull/613)
- Bump brace-expansion from 1.1.12 to 5.0.6 in [#​658](https://github.com/docker/metadata-action/pull/658) [#​630](https://github.com/docker/metadata-action/pull/630)
- Bump csv-parse from 6.1.0 to 6.2.1 in [#​617](https://github.com/docker/metadata-action/pull/617)
- Bump fast-xml-parser from 5.4.2 to 5.8.0 in [#​620](https://github.com/docker/metadata-action/pull/620)
- Bump flatted from 3.3.3 to 3.4.2 in [#​623](https://github.com/docker/metadata-action/pull/623)
- Bump glob from 10.3.15 to 10.5.0 in [#​621](https://github.com/docker/metadata-action/pull/621)
- Bump handlebars from 4.7.8 to 4.7.9 in [#​629](https://github.com/docker/metadata-action/pull/629)
- Bump lodash from 4.17.23 to 4.18.1 in [#​639](https://github.com/docker/metadata-action/pull/639)
- Bump moment-timezone from 0.6.0 to 0.6.1 in [#​619](https://github.com/docker/metadata-action/pull/619)
- Bump picomatch from 4.0.3 to 4.0.4 in [#​626](https://github.com/docker/metadata-action/pull/626)
- Bump postcss from 8.5.6 to 8.5.10 in [#​649](https://github.com/docker/metadata-action/pull/649)
- Bump tar from 6.2.1 to 7.5.15 in [#​657](https://github.com/docker/metadata-action/pull/657)
- Bump undici from 6.23.0 to 6.25.0 in [#​614](https://github.com/docker/metadata-action/pull/614)
- Bump vite from 7.3.1 to 7.3.2 in [#​637](https://github.com/docker/metadata-action/pull/637)
**Full Changelog**: <https://github.com/docker/metadata-action/compare/v6.0.0...v6.1.0>
### [`v6.0.0`](https://github.com/docker/metadata-action/releases/tag/v6.0.0)
[Compare Source](https://github.com/docker/metadata-action/compare/v5.10.0...v6.0.0)
- Node 24 as default runtime (requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later) by [@​crazy-max](https://github.com/crazy-max) in [#​605](https://github.com/docker/metadata-action/pull/605)
- List inputs now preserve `#` inside values while still supporting full-line `#` comments by [@​crazy-max](https://github.com/crazy-max) in [#​607](https://github.com/docker/metadata-action/pull/607)
- Switch to ESM and update config/test wiring by [@​crazy-max](https://github.com/crazy-max) in [#​602](https://github.com/docker/metadata-action/pull/602)
- Bump lodash from 4.17.21 to 4.17.23 in [#​588](https://github.com/docker/metadata-action/pull/588)
- Bump [@​actions/core](https://github.com/actions/core) from 1.11.1 to 3.0.0 in [#​599](https://github.com/docker/metadata-action/pull/599)
- Bump [@​actions/github](https://github.com/actions/github) from 6.0.1 to 9.0.0 in [#​597](https://github.com/docker/metadata-action/pull/597)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.68.0 to 0.79.0 in [#​604](https://github.com/docker/metadata-action/pull/604)
- Bump [@​isaacs/brace-expansion](https://github.com/isaacs/brace-expansion) from 5.0.0 to 5.0.1 in [#​600](https://github.com/docker/metadata-action/pull/600)
- Bump semver from 7.7.3 to 7.7.4 in [#​603](https://github.com/docker/metadata-action/pull/603)
**Full Changelog**: <https://github.com/docker/metadata-action/compare/v5.10.0...v6.0.0>
</details>
<details>
<summary>docker/setup-buildx-action (docker/setup-buildx-action)</summary>
### [`v4.4.1`](https://github.com/docker/setup-buildx-action/releases/tag/v4.4.1)
[Compare Source](https://github.com/docker/setup-buildx-action/compare/v4.4.0...v4.4.1)
- Skip BuildKit image pre-pulls for explicit endpoints by [@​crazy-max](https://github.com/crazy-max) in [#​624](https://github.com/docker/setup-buildx-action/pull/624)
**Full Changelog**: <https://github.com/docker/setup-buildx-action/compare/v4.4.0...v4.4.1>
### [`v4.4.0`](https://github.com/docker/setup-buildx-action/releases/tag/v4.4.0)
[Compare Source](https://github.com/docker/setup-buildx-action/compare/v4.3.0...v4.4.0)
- Use official Buildx releases for cloud driver by [@​crazy-max](https://github.com/crazy-max) in [#​606](https://github.com/docker/setup-buildx-action/pull/606)
- Pull BuildKit image before builder creation by [@​crazy-max](https://github.com/crazy-max) in [#​609](https://github.com/docker/setup-buildx-action/pull/609)
- Use shared error helpers for Buildx and Docker commands by [@​crazy-max](https://github.com/crazy-max) in [#​620](https://github.com/docker/setup-buildx-action/pull/620)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.95.0 to 0.100.0 in [#​610](https://github.com/docker/setup-buildx-action/pull/610) [#​618](https://github.com/docker/setup-buildx-action/pull/618) [#​619](https://github.com/docker/setup-buildx-action/pull/619)
- Bump [@​humanfs/node](https://github.com/humanfs/node) from 0.16.7 to 0.16.8 in [#​614](https://github.com/docker/setup-buildx-action/pull/614)
- Bump js-yaml from 5.3.0 to 5.4.2 in [#​608](https://github.com/docker/setup-buildx-action/pull/608) [#​617](https://github.com/docker/setup-buildx-action/pull/617)
- Bump postcss-selector-parser from 7.1.1 to 7.1.5 in [#​611](https://github.com/docker/setup-buildx-action/pull/611)
**Full Changelog**: <https://github.com/docker/setup-buildx-action/compare/v4.3.0...v4.4.0>
### [`v4.3.0`](https://github.com/docker/setup-buildx-action/releases/tag/v4.3.0)
[Compare Source](https://github.com/docker/setup-buildx-action/compare/v4.2.0...v4.3.0)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.92.0 to 0.95.0 in [#​595](https://github.com/docker/setup-buildx-action/pull/595)
- Bump brace-expansion from 1.1.13 to 1.1.18 in [#​600](https://github.com/docker/setup-buildx-action/pull/600)
- Bump js-yaml from 5.2.0 to 5.3.0 in [#​585](https://github.com/docker/setup-buildx-action/pull/585)
- Bump postcss from 8.5.10 to 8.5.25 in [#​598](https://github.com/docker/setup-buildx-action/pull/598)
- Bump undici from 6.27.0 to 6.28.0 in [#​601](https://github.com/docker/setup-buildx-action/pull/601)
**Full Changelog**: <https://github.com/docker/setup-buildx-action/compare/v4.2.0...v4.3.0>
### [`v4.2.0`](https://github.com/docker/setup-buildx-action/releases/tag/v4.2.0)
[Compare Source](https://github.com/docker/setup-buildx-action/compare/v4.1.0...v4.2.0)
- Preserve names in esbuild bundle by [@​crazy-max](https://github.com/crazy-max) in [#​572](https://github.com/docker/setup-buildx-action/pull/572)
- Bump [@​actions/core](https://github.com/actions/core) from 3.0.0 to 3.0.1 in [#​551](https://github.com/docker/setup-buildx-action/pull/551)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.90.0 to 0.92.0 in [#​557](https://github.com/docker/setup-buildx-action/pull/557) [#​580](https://github.com/docker/setup-buildx-action/pull/580)
- Bump [@​sigstore/core](https://github.com/sigstore/core) from 3.1.0 to 3.2.1 in [#​573](https://github.com/docker/setup-buildx-action/pull/573)
- Bump [@​sigstore/verify](https://github.com/sigstore/verify) from 3.1.0 to 3.1.1 in [#​576](https://github.com/docker/setup-buildx-action/pull/576)
- Bump js-yaml from 4.1.1 to 5.2.0 in [#​562](https://github.com/docker/setup-buildx-action/pull/562)
- Bump sigstore from 4.1.0 to 4.1.1 in [#​577](https://github.com/docker/setup-buildx-action/pull/577)
- Bump tmp from 0.2.5 to 0.2.7 in [#​556](https://github.com/docker/setup-buildx-action/pull/556)
- Bump undici from 6.25.0 to 6.27.0 in [#​570](https://github.com/docker/setup-buildx-action/pull/570)
- Bump vite from 7.3.2 to 7.3.6 in [#​569](https://github.com/docker/setup-buildx-action/pull/569)
**Full Changelog**: <https://github.com/docker/setup-buildx-action/compare/v4.1.0...v4.2.0>
### [`v4.1.0`](https://github.com/docker/setup-buildx-action/releases/tag/v4.1.0)
[Compare Source](https://github.com/docker/setup-buildx-action/compare/v4.0.0...v4.1.0)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.79.0 to 0.90.0 in [#​489](https://github.com/docker/setup-buildx-action/pull/489)
- Bump brace-expansion from 1.1.12 to 5.0.6 in [#​547](https://github.com/docker/setup-buildx-action/pull/547) [#​508](https://github.com/docker/setup-buildx-action/pull/508)
- Bump fast-xml-builder from 1.0.0 to 1.2.0 in [#​540](https://github.com/docker/setup-buildx-action/pull/540)
- Bump fast-xml-parser from 5.4.2 to 5.8.0 in [#​496](https://github.com/docker/setup-buildx-action/pull/496)
- Bump flatted from 3.3.3 to 3.4.2 in [#​499](https://github.com/docker/setup-buildx-action/pull/499)
- Bump glob from 10.3.12 to 13.0.6 in [#​495](https://github.com/docker/setup-buildx-action/pull/495)
- Bump handlebars from 4.7.8 to 4.7.9 in [#​504](https://github.com/docker/setup-buildx-action/pull/504)
- Bump lodash from 4.17.23 to 4.18.1 in [#​523](https://github.com/docker/setup-buildx-action/pull/523)
- Bump picomatch from 4.0.3 to 4.0.4 in [#​503](https://github.com/docker/setup-buildx-action/pull/503)
- Bump postcss from 8.5.6 to 8.5.10 in [#​537](https://github.com/docker/setup-buildx-action/pull/537)
- Bump tar from 6.2.1 to 7.5.15 in [#​545](https://github.com/docker/setup-buildx-action/pull/545)
- Bump undici from 6.23.0 to 6.25.0 in [#​492](https://github.com/docker/setup-buildx-action/pull/492)
- Bump vite from 7.3.1 to 7.3.2 in [#​520](https://github.com/docker/setup-buildx-action/pull/520)
**Full Changelog**: <https://github.com/docker/setup-buildx-action/compare/v4.0.0...v4.1.0>
### [`v4.0.0`](https://github.com/docker/setup-buildx-action/releases/tag/v4.0.0)
[Compare Source](https://github.com/docker/setup-buildx-action/compare/v3.12.0...v4.0.0)
- Node 24 as default runtime (requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later) by [@​crazy-max](https://github.com/crazy-max) in [#​483](https://github.com/docker/setup-buildx-action/pull/483)
- Remove deprecated inputs/outputs by [@​crazy-max](https://github.com/crazy-max) in [#​464](https://github.com/docker/setup-buildx-action/pull/464)
- Switch to ESM and update config/test wiring by [@​crazy-max](https://github.com/crazy-max) in [#​481](https://github.com/docker/setup-buildx-action/pull/481)
- Bump [@​actions/core](https://github.com/actions/core) from 1.11.1 to 3.0.0 in [#​475](https://github.com/docker/setup-buildx-action/pull/475)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.63.0 to 0.79.0 in [#​482](https://github.com/docker/setup-buildx-action/pull/482) [#​485](https://github.com/docker/setup-buildx-action/pull/485)
- Bump js-yaml from 4.1.0 to 4.1.1 in [#​452](https://github.com/docker/setup-buildx-action/pull/452)
- Bump lodash from 4.17.21 to 4.17.23 in [#​472](https://github.com/docker/setup-buildx-action/pull/472)
- Bump minimatch from 3.1.2 to 3.1.5 in [#​480](https://github.com/docker/setup-buildx-action/pull/480)
**Full Changelog**: <https://github.com/docker/setup-buildx-action/compare/v3.12.0...v4.0.0>
</details>
<details>
<summary>docker/setup-qemu-action (docker/setup-qemu-action)</summary>
### [`v4.4.0`](https://github.com/docker/setup-qemu-action/releases/tag/v4.4.0)
[Compare Source](https://github.com/docker/setup-qemu-action/compare/v4.3.0...v4.4.0)
- Use the shared error helper for Docker commands by [@​crazy-max](https://github.com/crazy-max) in [#​345](https://github.com/docker/setup-qemu-action/pull/345)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.96.0 to 0.100.0 in [#​344](https://github.com/docker/setup-qemu-action/pull/344)
- Bump [@​humanfs/node](https://github.com/humanfs/node) from 0.16.7 to 0.16.8 in [#​340](https://github.com/docker/setup-qemu-action/pull/340)
- Bump js-yaml from 4.3.1 to 4.3.2 in [#​342](https://github.com/docker/setup-qemu-action/pull/342)
- Bump postcss-selector-parser from 7.1.1 to 7.1.5 in [#​337](https://github.com/docker/setup-qemu-action/pull/337)
**Full Changelog**: <https://github.com/docker/setup-qemu-action/compare/v4.3.0...v4.4.0>
### [`v4.3.0`](https://github.com/docker/setup-qemu-action/releases/tag/v4.3.0)
[Compare Source](https://github.com/docker/setup-qemu-action/compare/v4.2.0...v4.3.0)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.92.0 to 0.96.0 in [#​336](https://github.com/docker/setup-qemu-action/pull/336)
- Bump [@​sigstore/verify](https://github.com/sigstore/verify) from 3.1.0 to 3.1.1 in [#​316](https://github.com/docker/setup-qemu-action/pull/316)
- Bump brace-expansion from 1.1.15 to 1.1.18 in [#​332](https://github.com/docker/setup-qemu-action/pull/332)
- Bump js-yaml from 4.2.0 to 4.3.1 in [#​334](https://github.com/docker/setup-qemu-action/pull/334)
- Bump postcss from 8.5.10 to 8.5.25 in [#​331](https://github.com/docker/setup-qemu-action/pull/331)
- Bump sigstore from 4.1.0 to 4.1.1 in [#​317](https://github.com/docker/setup-qemu-action/pull/317)
- Bump undici from 6.27.0 to 6.28.0 in [#​333](https://github.com/docker/setup-qemu-action/pull/333)
**Full Changelog**: <https://github.com/docker/setup-qemu-action/compare/v4.2.0...v4.3.0>
### [`v4.2.0`](https://github.com/docker/setup-qemu-action/releases/tag/v4.2.0)
[Compare Source](https://github.com/docker/setup-qemu-action/compare/v4.1.0...v4.2.0)
- Preserve names in esbuild bundle by [@​crazy-max](https://github.com/crazy-max) in [#​311](https://github.com/docker/setup-qemu-action/pull/311)
- Bump [@​actions/core](https://github.com/actions/core) from 3.0.0 to 3.0.1 in [#​295](https://github.com/docker/setup-qemu-action/pull/295)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.91.0 to 0.92.0 in [#​315](https://github.com/docker/setup-qemu-action/pull/315)
- Bump [@​sigstore/core](https://github.com/sigstore/core) from 3.1.0 to 3.2.1 in [#​312](https://github.com/docker/setup-qemu-action/pull/312)
- Bump js-yaml from 4.1.1 to 4.2.0 in [#​310](https://github.com/docker/setup-qemu-action/pull/310)
- Bump tmp from 0.2.6 to 0.2.7 in [#​304](https://github.com/docker/setup-qemu-action/pull/304)
- Bump undici from 6.26.0 to 6.27.0 in [#​308](https://github.com/docker/setup-qemu-action/pull/308)
- Bump vite from 7.3.2 to 7.3.6 in [#​307](https://github.com/docker/setup-qemu-action/pull/307)
**Full Changelog**: <https://github.com/docker/setup-qemu-action/compare/v4.1.0...v4.2.0>
### [`v4.1.0`](https://github.com/docker/setup-qemu-action/releases/tag/v4.1.0)
[Compare Source](https://github.com/docker/setup-qemu-action/compare/v4.0.0...v4.1.0)
- Add `reset` input to uninstall current emulators by [@​crazy-max](https://github.com/crazy-max) in [#​21](https://github.com/docker/setup-qemu-action/pull/21)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.77.0 to 0.91.0 in [#​250](https://github.com/docker/setup-qemu-action/pull/250) [#​247](https://github.com/docker/setup-qemu-action/pull/247)
- Bump brace-expansion from 1.1.12 to 1.1.15 in [#​265](https://github.com/docker/setup-qemu-action/pull/265)
- Bump fast-xml-builder from 1.0.0 to 1.2.0 in [#​286](https://github.com/docker/setup-qemu-action/pull/286)
- Bump fast-xml-parser from 5.4.2 to 5.8.0 in [#​255](https://github.com/docker/setup-qemu-action/pull/255)
- Bump flatted from 3.3.3 to 3.4.2 in [#​257](https://github.com/docker/setup-qemu-action/pull/257)
- Bump glob from 10.3.15 to 10.5.0 in [#​254](https://github.com/docker/setup-qemu-action/pull/254)
- Bump handlebars from 4.7.8 to 4.7.9 in [#​262](https://github.com/docker/setup-qemu-action/pull/262)
- Bump lodash from 4.17.23 to 4.18.1 in [#​273](https://github.com/docker/setup-qemu-action/pull/273)
- Bump postcss from 8.5.6 to 8.5.10 in [#​285](https://github.com/docker/setup-qemu-action/pull/285)
- Bump tar from 6.2.1 to 7.5.15 in [#​287](https://github.com/docker/setup-qemu-action/pull/287)
- Bump tmp from 0.2.5 to 0.2.6 in [#​291](https://github.com/docker/setup-qemu-action/pull/291)
- Bump undici from 6.23.0 to 6.26.0 in [#​251](https://github.com/docker/setup-qemu-action/pull/251)
- Bump vite from 7.3.1 to 7.3.2 in [#​271](https://github.com/docker/setup-qemu-action/pull/271)
**Full Changelog**: <https://github.com/docker/setup-qemu-action/compare/v4.0.0...v4.1.0>
### [`v4.0.0`](https://github.com/docker/setup-qemu-action/releases/tag/v4.0.0)
[Compare Source](https://github.com/docker/setup-qemu-action/compare/v3.7.0...v4.0.0)
- Node 24 as default runtime (requires [Actions Runner v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) or later) by [@​crazy-max](https://github.com/crazy-max) in [#​245](https://github.com/docker/setup-qemu-action/pull/245)
- Switch to ESM and update config/test wiring by [@​crazy-max](https://github.com/crazy-max) in [#​241](https://github.com/docker/setup-qemu-action/pull/241)
- Bump [@​actions/core](https://github.com/actions/core) from 1.11.1 to 3.0.0 in [#​244](https://github.com/docker/setup-qemu-action/pull/244)
- Bump [@​docker/actions-toolkit](https://github.com/docker/actions-toolkit) from 0.67.0 to 0.77.0 in [#​243](https://github.com/docker/setup-qemu-action/pull/243)
- Bump [@​isaacs/brace-expansion](https://github.com/isaacs/brace-expansion) from 5.0.0 to 5.0.1 in [#​240](https://github.com/docker/setup-qemu-action/pull/240)
- Bump js-yaml from 3.14.1 to 3.14.2 in [#​231](https://github.com/docker/setup-qemu-action/pull/231)
- Bump lodash from 4.17.21 to 4.17.23 in [#​238](https://github.com/docker/setup-qemu-action/pull/238)
**Full Changelog**: <https://github.com/docker/setup-qemu-action/compare/v3.7.0...v4.0.0>
</details>
<details>
<summary>jsdom/jsdom (jsdom)</summary>
### [`v30.1.1`](https://github.com/jsdom/jsdom/releases/tag/v30.1.1)
[Compare Source](https://github.com/jsdom/jsdom/compare/v30.1.0...v30.1.1)
- Fixed spurious window `blur` and `focusout` events and incorrect `event.relatedTarget` values when focusing an element after removing the previously focused element, which regressed in v30.1.0. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed focus and blur behavior across frames, and focusing the document's viewport through `document.documentElement.focus()`. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed focus targets removed or disabled by `blur` listeners becoming active, and text selections made by focus and blur listeners being overwritten. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed `element.focus()` incorrectly focusing disabled form controls and `<input type="hidden">` elements with `tabindex=""`. ([@​scttcper](https://github.com/scttcper))
- Fixed invalid `style.setProperty()` calls changing existing `!important` priorities, serialized styles, or mutation records. ([@​FedgeNo](https://github.com/FedgeNo))
- Fixed `!important` handling when updating CSS longhands after shorthands, using variables or CSS-wide keywords, and assigning style properties directly. ([@​FedgeNo](https://github.com/FedgeNo))
- Fixed `<noscript>` parsing with `includeNodeLocations: true` or inside frames to honor the `runScripts` option.
- Fixed the `storageQuota` option being ignored by frames.
- Fixed encoding detection of HTML and XML byte input to honor XML encoding declarations and detect UTF-16 without a byte order mark.
- Fixed exceptions caused by truncated `charset` parameters in `<meta>` elements, and encoding detection incorrectly using incomplete `<meta>` tags. ([@​FedgeNo](https://github.com/FedgeNo))
- Fixed XML serialization errors for namespaces named `constructor`, `toString`, `__proto__`, or `"null"`, and incorrect reuse of namespace prefixes declared on sibling elements.
- Fixed `element.innerHTML` and `element.outerHTML` in XML documents to reject invalid characters in attribute values and avoid stack overflows on large strings.
- Fixed selector matching for `:lang()`, `:nth-child(... of ...)` after mutations, and `:has()` with duplicate IDs or nested logical pseudo-classes. ([@​asamuzaK](https://github.com/asamuzaK))
### [`v30.1.0`](https://github.com/jsdom/jsdom/releases/tag/v30.1.0)
[Compare Source](https://github.com/jsdom/jsdom/compare/v30.0.1...v30.1.0)
jsdom is feeling the AGI!
This release is dedicated to [@​scttcper](https://github.com/scttcper), who unleashed [@​codex](https://github.com/codex) upon jsdom and found tons of performance improvements. Along the way, he found and fixed many correctness issues as well.
We really appreciate his thoughtful PRs, which did a great job following the project's contribution guidelines, and were clearly human-curated, with their PR descriptions edited to be brief and respectful of the maintainers' time.
Thanks to [@​scttcper](https://github.com/scttcper), as well as all the other contributors of this release (most of whom were AI-assisted).
- Added named access to elements on `document`, such as `document.myForm` for `<form name="myForm">`. ([@​vojtisprime11](https://github.com/vojtisprime11))
- Added `QuotaExceededError`, including its use for storage quota errors and oversized `crypto.getRandomValues()` requests.
- Added support for the relaxed DOM naming rules when creating elements, attributes, and document types.
- Improved performance of DOM construction, tree mutations, range operations, and live collection access, especially on large documents. ([@​scttcper](https://github.com/scttcper), [@​erezrokah](https://github.com/erezrokah))
- Improved performance of `getComputedStyle()`, style changes, and CSS serialization. ([@​scttcper](https://github.com/scttcper), [@​jhult](https://github.com/jhult))
- Improved performance of event dispatch, form control and label lookups, and updates to `<select>` elements and radio button groups. ([@​scttcper](https://github.com/scttcper))
- Reduced memory use when creating and working with DOM nodes, attributes, event listeners, and mutation observers. ([@​scttcper](https://github.com/scttcper))
- Changed `window.close()` to preserve access to the document and its DOM through retained references.
- Fixed `element.querySelectorAll()` returning no matches when the first part of the selector matches the element itself, which regressed in v30.0.0. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed case sensitivity in CSS attribute selectors, including selectors matching `data-state=""`, `title=""`, and other case-sensitive values. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed `document.querySelector()` failing to find a matching element when an earlier element has the same ID but does not match the rest of the selector. ([@​vojtisprime11](https://github.com/vojtisprime11))
- Fixed `:focus` matching in shadow trees. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed DOM insertion and replacement, including valid `document.replaceChildren()` calls, invalid document element and doctype placements, and mutations during `element.replaceWith()`.
- Fixed the ordering of script execution, custom element callbacks, iframe loading, and mutation observer notifications during DOM insertion, including in shadow trees.
- Fixed queued events and navigation continuing after `window.close()` or iframe removal, and prevented new scripts, resource loads, timers, and animation frames from starting in destroyed documents. ([@​scttcper](https://github.com/scttcper))
- Fixed parent documents waiting indefinitely for loading to finish when a child iframe removes itself during loading.
- Fixed request cancellation across redirects, during pending `requestInterceptor()` callbacks, and when reusing an `XMLHttpRequest` after aborting it.
- Fixed resource loading and `JSDOM.fromURL()` potentially hanging when response handling throws and response stream cleanup does not finish.
- Fixed successful cached resource loads being treated as aborted.
- Fixed `getComputedStyle()` and `document.styleSheets` using the wrong stylesheet order after inserting or updating `<style>` elements.
- Fixed `getComputedStyle()` ignoring nested `@import` and `@media` rules in imported stylesheets, and returning stale results after imports finish loading.
- Fixed style invalidation, stylesheet removal, and frame source updates in shadow trees.
- Fixed repeated `getComputedStyle()` calls changing case-sensitive background URLs, and inconsistent resolution of border shorthands containing system colors. ([@​scttcper](https://github.com/scttcper))
- Fixed computed border widths, including borderless elements incorrectly reporting `16px`, which regressed in v30.0.0. ([@​Alberto-BaseNet](https://github.com/Alberto-BaseNet))
- Fixed `getComputedStyle()` to resolve `'font-weight'` keywords to numeric values. ([@​tianrking](https://github.com/tianrking))
- Fixed `getComputedStyle()` to convert lengths to pixels inside CSS math functions containing percentages, and to resolve percentages in `'font-size'` math functions. ([@​soroushm](https://github.com/soroushm))
- Fixed serialization of `min()` and `max()` containing nested `calc()`, which regressed in v30.0.0. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed CSS values mixing lengths or percentages with math functions, such as `'grid-template-columns'` values containing both `100px` and `calc()`. ([@​rome-xi](https://github.com/rome-xi))
- Fixed parsing of `'background'` and `'border'` shorthands with adjacent components, such as `url(a.png)no-repeat`, including a crash when parsing inline styles. Also fixed handling of invalid shorthand assignments and escaped or unusual characters in CSS declarations. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed parsing of unitless zero values in `'flex'` shorthands, such as `35 1 0`, and rejection of negative `'flex-basis'` lengths and percentages. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed shorthand style assignments producing extra mutation records and custom element callbacks for intermediate values. ([@​scttcper](https://github.com/scttcper))
- Fixed `Range` and `Selection` handling of CDATA sections, including boundary offsets and range cloning, extraction, deletion, insertion, and stringification. ([@​scttcper](https://github.com/scttcper))
- Fixed `text.normalize()` incorrectly removing the text node or merging its siblings. ([@​scttcper](https://github.com/scttcper))
- Fixed cloning and importing CDATA sections and processing instructions whose contents have been modified, and serialization of CDATA sections adopted into HTML documents.
- Fixed stale named-property collections on `window`, and incorrect named access from empty or namespaced `id=""` and `name=""` values. ([@​scttcper](https://github.com/scttcper))
- Fixed elements in documents created with `DOMParser` or `document.implementation.createHTMLDocument()` appearing as named properties on `window` and being retained in memory. ([@​Iaotle](https://github.com/Iaotle))
- Fixed memory leaks from mutation observers retaining observed nodes, abort signals retaining removed event listeners, and storage event tracking retaining closed windows. ([@​scttcper](https://github.com/scttcper))
- Fixed `storage` events being sent to windows created after the storage change, and ensured surviving recipients still receive events when the source document is destroyed.
- Fixed attribute lookups after namespace prefix changes, and namespaced attributes incorrectly affecting ID lookups and element behavior. ([@​scttcper](https://github.com/scttcper))
- Fixed `input.list` in detached element trees. ([@​scttcper](https://github.com/scttcper))
- Fixed `attr.ownerDocument` after setting an attribute node on an element in another document or adopting its element. ([@​Kjubikstronk](https://github.com/Kjubikstronk))
- Fixed fresh `element.getElementsByTagName()` calls using the previous document's case-sensitivity rules after adoption between HTML and XML documents. ([@​Kjubikstronk](https://github.com/Kjubikstronk))
- Fixed `element.tagName` returning stale casing after adoption between HTML and XML documents.
- Fixed radio button grouping and checkedness updates for controls with different form owners, controls outside their form, unnamed controls, and canceled clicks.
- Fixed `<select>` selection updates when moving options, and unintended selection resets when moving a whole `<select>` or changing unrelated descendants.
- Fixed cloning `<input>` elements to preserve `input.indeterminate`.
- Fixed focus state after removing a focused element or its ancestor, including inside shadow trees.
- Fixed script execution when inserting children into a connected, empty `<script>`. ([@​Kjubikstronk](https://github.com/Kjubikstronk))
- Fixed incorrect script execution after changes to `src=""`, and handling of `type=""`, `for=""`, and `event=""`.
- Fixed `document.currentScript` during nested script execution and scripts in shadow trees.
- Fixed event dispatch, `window.event`, and default passive listener handling for non-node event targets, and event dispatch when user code modifies `window.constructor`.
- Fixed `volumechange` and `ratechange` events to fire asynchronously. ([@​christianaurichzm](https://github.com/christianaurichzm))
- Fixed DOM APIs accepting user-created proxies around DOM objects where genuine DOM objects are required.
- Fixed XPath iterator invalidation after DOM mutations and errors for invalid result types.
- Fixed `NodeIterator` traversal when its filter removes nodes.
- Fixed `fileReader.readAsText()` to honor the blob's MIME type charset when no supported explicit encoding is supplied.
- Fixed fractional seconds in time input parsing and serialization. ([@​Jaybhade](https://github.com/Jaybhade))
- Fixed large `input.valueAsNumber` assignments for time and local date/time inputs.
- Fixed hard wrapping of `<textarea>` values to use the numeric `textarea.cols` value.
- Fixed `document.readyState` to be `"complete"` for documents created with `document.implementation.createDocument()` and `document.implementation.createHTMLDocument()`.
- Fixed `element.translate` for empty `translate=""` values and elements without a parent.
- Fixed XML parsing of lone surrogates to replace them with the Unicode replacement character.
- Fixed `node.lookupNamespaceURI()` for the `xml` and `xmlns` prefixes. ([@​vojtisprime11](https://github.com/vojtisprime11))
- Fixed `<base>` elements to ignore `data:` and `javascript:` URLs. ([@​vojtisprime11](https://github.com/vojtisprime11))
- Fixed `svgElement.viewportElement` inside `<symbol>` elements.
- Fixed the default `blobEvent.timecode` to be `NaN`.
- Fixed the object shape and property descriptors of `CSS`.
### [`v30.0.1`](https://github.com/jsdom/jsdom/releases/tag/v30.0.1)
[Compare Source](https://github.com/jsdom/jsdom/compare/v30.0.0...v30.0.1)
- Fixed `getComputedStyle()` with `calc()` and other functions throwing an exception, which regressed in v30.0.0. ([@​asamuzaK](https://github.com/asamuzaK))
- Sped up up range operations on large documents ([@​leonidaz](https://github.com/leonidaz))
### [`v30.0.0`](https://github.com/jsdom/jsdom/releases/tag/v30.0.0)
[Compare Source](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.0.0)
Breaking changes:
- Node.js minimum version raised to `^22.22.2 || ^24.15.0 || >=26.0.0`.
Other changes:
- Added `CSS.escape()` and `CSS.supports()` functions. ([@​asamuzaK](https://github.com/asamuzaK))
- Added `'background-position-x'` and `'background-position-y'` CSS properties. ([@​olagokemills](https://github.com/olagokemills))
- Fixed `getComputedStyle()` to convert length values into pixels. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed CSS function serialization, e.g., in the return value of `getPropertyValue()`. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed the type of error thrown by `document.evaluate()` ([@​dokson](https://github.com/dokson))
### [`v29.1.1`](https://github.com/jsdom/jsdom/releases/tag/v29.1.1)
[Compare Source](https://github.com/jsdom/jsdom/compare/v29.1.0...v29.1.1)
- Fixed `'border-radius'` computed style serialization. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed computed style computation when using `'background-origin'` and `'background-clip'` CSS properties. ([@​asamuzaK](https://github.com/asamuzaK))
- Significantly optimized initial calls to `getComputedStyle()`, before the cache warms up. ([@​asamuzaK](https://github.com/asamuzaK))
### [`v29.1.0`](https://github.com/jsdom/jsdom/releases/tag/v29.1.0)
[Compare Source](https://github.com/jsdom/jsdom/compare/v29.0.2...v29.1.0)
- Added basic support for the ratio CSS type. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed `getComputedStyle()` sometimes returning outdated results after CSS was modified. ([@​asamuzaK](https://github.com/asamuzaK))
### [`v29.0.2`](https://github.com/jsdom/jsdom/releases/tag/v29.0.2)
[Compare Source](https://github.com/jsdom/jsdom/compare/v29.0.1...v29.0.2)
- Significantly improved and sped up `getComputedStyle()`. Computed value rules are now applied across a broader set of properties, and include fixes related to inheritance, defaulting keywords, custom properties, and color-related values such as `currentcolor` and system colors. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed CSS `'background`' and `'border'` shorthand parsing. ([@​asamuzaK](https://github.com/asamuzaK))
### [`v29.0.1`](https://github.com/jsdom/jsdom/releases/tag/v29.0.1)
[Compare Source](https://github.com/jsdom/jsdom/compare/v29.0.0...v29.0.1)
- Fixed CSS parsing of `'border'`, `'background'`, and their sub-shorthands containing keywords or `var()`. ([@​asamuzaK](https://github.com/asamuzaK))
- Fixed `getComputedStyle()` to return a more functional `CSSStyleDeclaration` object, including indexed access support, which regressed in v29.0.0.
### [`v29.0.0`](https://github.com/jsdom/jsdom/releases/tag/v29.0.0)
[Compare Source](https://github.com/jsdom/jsdom/compare/v28.1.0...v29.0.0)
Breaking changes:
- Node.js v22.13.0+ is now the minimum supported v22 version (was v22.12.0+).
Other changes:
- Overhauled the CSSOM implementation, replacing the [`@acemir/cssom`](https://www.npmjs.com/package/@acemir/cssom) and [`cssstyle`](https://github.com/jsdom/cssstyle) dependencies with fresh internal implementations built on webidl2js wrappers and the [`css-tree`](https://www.npmjs.com/package/css-tree) parser. Serialization, parsing, and API behavior is improved in various ways, especially around edge cases.
- Added `CSSCounterStyleRule` and `CSSNamespaceRule` to jsdom `Window`s.
- Added `cssMediaRule.matches` and `cssSupportsRule.matches` getters.
- Added proper media query parsing in `MediaList`, using `css-tree` instead of naive comma-splitting. Invalid queries become `"not all"` per spec.
- Added `cssKeyframeRule.keyText` getter/setter validation.
- Added `cssStyleRule.selectorText` setter validation: invalid selectors are now rejected.
- Added `styleSheet.ownerNode`, `styleSheet.href`, and `styleSheet.title`.
- Added bad port blocking per the [fetch specification](https://fetch.spec.whatwg.org/#bad-port), preventing fetches to commonly-abused ports.
- Improved `Document` initialization performance by lazily initializing the CSS selector engine, avoiding \~0.5 ms of overhead per `Document`. ([@​thypon](https://github.com/thypon))
- Fixed a memory leak when stylesheets were removed from the document.
- Fixed `CSSStyleDeclaration` modifications to properly trigger custom element reactions.
- Fixed nested `@media` rule parsing.
- Fixed `CSSStyleSheet`'s "disallow modification" flag not being checked in all mutation methods.
- Fixed `XMLHttpRequest`'s `response` getter returning parsed JSON during the `LOADING` state instead of `null`.
- Fixed `getComputedStyle()` crashing in XHTML documents when stylesheets contained at-rules such as `@page` or `@font-face`.
- Fixed a potential hang in synchronous `XMLHttpRequest` caused by a race condition with the worker thread's idle timeout.
</details>
<details>
<summary>pnpm/pnpm (pnpm)</summary>
### [`v12.6.0`](https://github.com/pnpm/pnpm/releases/tag/v12.6.0): pnpm 12.6
[Compare Source](https://github.com/pnpm/pnpm/compare/pnpm@12.5.1...v12.6.0)
pnpm 12.6.0 ships with automatic dependency deduplication, relocatable node\_modules, package.yaml manifest editing, and --save-types support.
##### Minor Changes
- `autoDedupe` deduplicates compatible dependency versions during installation [#​7258](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/2008). Existing comments and key order are preserved.
- Catalog entries can now use the `file:` and `link:` protocols [#​8642](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/12863). The Yarn `-T` shorthand is not supported.
- `progress` setting and `--no-progress` option now turn off dependency and download progress lines [#​14065](https://github.com/pnpm/pnpm/issues/14065). Warnings, lifecycle output, and the dependency summary are still printed.
##### Patch Changes
##### Security
- 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](https://github.com/pnpm/pnpm/issues/14866).
- `pnpm install` warnings no longer carry the text of a package's deprecation notice, naming only the deprecated package and version [#​15099](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15051).
##### Installing packages
- `pnpm install --frozen-lockfile` now succeeds when an optional dependency was unresolvable and skipped by the install that wrote the lockfile [#​3960](https://github.com/pnpm/pnpm/issues/3960).
- `pnpm install --frozen-lockfile` no longer installs dependencies of projects removed from `pnpm-workspace.yaml` [#​15248](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15276).
- `pnpm install --force` now re-imports every package into the virtual store [#​15030](https://github.com/pnpm/pnpm/issues/15030) and removes obsolete dependency links inside virtual-store packages when their dependencies change [#​15039](https://github.com/pnpm/pnpm/issues/15039).
- `preinstall` script for the root project now runs before dependencies are resolved and linked [#​3760](https://github.com/pnpm/pnpm/issues/3760).
- `pnpm install` now runs `pnpm:devPreinstall` when the root project uses `package.yaml` [#​15168](https://github.com/pnpm/pnpm/issues/15168).
- `pnpm install` now enforces the root project's `engines.node` range when `engineStrict` is enabled [#​3016](https://github.com/pnpm/pnpm/issues/3016).
- `pnpm install` now uses the running Node.js when `devEngines.runtime` declares a range without `onFail: download` [#​15230](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/2227).
- `pnpm install` now installs git-hosted dependencies without preparing them when their builds are explicitly denied by `allowBuilds` [#​10522](https://github.com/pnpm/pnpm/issues/10522).
- `pnpm install` now reuses an in-flight tarball download when another resolution of the same archive still needs its `package.json` [#​15037](https://github.com/pnpm/pnpm/issues/15037).
- `pnpm install --prod` no longer downloads registry packages that only a devDependency reaches [#​881](https://github.com/pnpm/pnpm/issues/881).
- `pnpm install --no-runtime --frozen-lockfile` with `nodeLinker: hoisted` no longer fails on repeated runs with a broken lockfile [#​15212](https://github.com/pnpm/pnpm/issues/15212).
##### Resolving and linking dependencies
- `pnpm install` and `pnpm update` now resolve a dependency range to the newest matching version that is not deprecated [#​15128](https://github.com/pnpm/pnpm/issues/15128).
- `pnpm add <pkg>` without a version now uses the catalog entry when the workspace already catalogs that package [#​14865](https://github.com/pnpm/pnpm/issues/14865).
- `pnpm install` now links workspace dependencies declared with plain version ranges when `excludeLinksFromLockfile` and `linkWorkspacePackages` are enabled [#​15133](https://github.com/pnpm/pnpm/issues/15133).
- `pnpm install` now resolves local tarball dependencies whose absolute `file:` paths contain `..` consistently and skips reinstallation on repeat installs [#​15190](https://github.com/pnpm/pnpm/issues/15190).
- `pnpm install` now installs dependencies when a custom resolver returns a local or git-hosted tarball without a manifest [#​15016](https://github.com/pnpm/pnpm/issues/15016).
- `pnpm.overrides` entries written as a bare path, such as `./local-dep`, are now measured from the directory holding `pnpm-workspace.yaml` [#​11131](https://github.com/pnpm/pnpm/issues/11131).
- `pnpm update --no-save` no longer bypasses version-scoped overrides when a dependency selector specifies a version [#​14923](https://github.com/pnpm/pnpm/issues/14923).
- `pnpm peers check` and strict peer dependency checks no longer reject compatible versions from named registries [#​15225](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15226).
- Workspace projects selected by `hoistPattern` or `publicHoistPattern` are now hoisted on every install [#​3642](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/2812).
- Sped up `pnpm dedupe` and `pnpm install` in projects with many convergence overrides by checking overrides concurrently [#​15175](https://github.com/pnpm/pnpm/issues/15175).
- `minimumReleaseAge` is no longer skipped for packages served by registries returning matching ETags for abbreviated and full package metadata [#​14925](https://github.com/pnpm/pnpm/issues/14925).
##### Running scripts and tasks
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15272).
- `pnpm test` now forwards `--filter` arguments to the test script when the option follows the shortcut [#​15217](https://github.com/pnpm/pnpm/issues/15217).
- Recursive runs now start scripts matched by a `/pattern/` selector in parallel within `workspaceConcurrency` [#​14933](https://github.com/pnpm/pnpm/issues/14933).
- `pnpm deploy`, `pnpm rebuild`, `pnpm rb`, and `pnpm setup` now prefer a `package.json` script of the same name [#​14976](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/3604).
- `pnpm install-test` now accepts `--no-bail` directly and in recursive runs [#​3777](https://github.com/pnpm/pnpm/issues/3777).
##### Workspace and project configuration
- `pnpm` commands run in a project not included in the workspace now act on that project alone [#​3561](https://github.com/pnpm/pnpm/issues/3561).
- `pnpm-workspace.yaml` edits now preserve scalar YAML anchors and aliases [#​8245](https://github.com/pnpm/pnpm/issues/8245).
- `pnpm-workspace.yaml` now expands environment variable placeholders with fallback syntax in enum-valued settings such as `nodeLinker` [#​14914](https://github.com/pnpm/pnpm/issues/14914).
- `pnpmfile` configuration now loads a `.js` file as CommonJS or an ES module, following the nearest `package.json` [#​15141](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15047), [#​15049](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/3735), [#​15136](https://github.com/pnpm/pnpm/issues/15136).
- `package.yaml` projects now record their pinned pnpm under `packageManagerDependencies` in `pnpm-lock.yaml` [#​15167](https://github.com/pnpm/pnpm/issues/15167).
- `packageManagerDependencies` pinning `@pnpm/exe` beside `pnpm` is no longer rewritten in `pnpm-lock.yaml` [#​14926](https://github.com/pnpm/pnpm/issues/14926).
- `pnpm` now preserves CRLF line endings when modifying project manifests [#​3529](https://github.com/pnpm/pnpm/issues/3529).
- `loglevel` setting is now honored when configured in `pnpm-workspace.yaml`, global configuration, or `PNPM_CONFIG_LOGLEVEL` [#​3122](https://github.com/pnpm/pnpm/issues/3122).
- `storeDir` values loaded from global configuration or `PNPM_CONFIG_STORE_DIR` now expand a leading `~/` to the user's home directory [#​6560](https://github.com/pnpm/pnpm/issues/6560).
- `--shared-workspace-lockfile` now produces a warning when passed on the command line outside a workspace [#​1617](https://github.com/pnpm/pnpm/issues/1617).
##### Windows
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15114), [#​15176](https://github.com/pnpm/pnpm/issues/15176), [#​15171](https://github.com/pnpm/pnpm/issues/15171).
- `pn`, `pnpx`, `pnx`, and `pnpm` now run when Git Bash, MSYS2, or Cygwin launches them through a Windows path [#​14884](https://github.com/pnpm/pnpm/issues/14884).
- `pnpm dlx` now reuses cached packages when Windows creates directory junctions for its cache links [#​15171](https://github.com/pnpm/pnpm/issues/15171).
- `pnpm pipeline --watch` now resolves Windows short paths so multiple path representations share the build cache [#​15105](https://github.com/pnpm/pnpm/issues/15105).
##### CLI commands and output
- `pnpm remove` now runs the project's own `preuninstall`, `uninstall`, and `postuninstall` scripts [#​3276](https://github.com/pnpm/pnpm/issues/3276).
- `pnpm remove -r` now fails before modifying manifests if any requested dependency is absent from all selected projects [#​2319](https://github.com/pnpm/pnpm/issues/2319).
- `pnpm update --peer` now updates ranges in `peerDependencies` [#​8081](https://github.com/pnpm/pnpm/issues/8081).
- `pnpm update` now moves `devEngines.runtime` and `engines.runtime` version ranges to the resolved Node.js version [#​14988](https://github.com/pnpm/pnpm/issues/14988).
- `pnpm update -g` no longer reinstalls unchanged packages [#​12002](https://github.com/pnpm/pnpm/issues/12002).
- `pnpm add -g`, `pnpm update -g`, and `pnpm remove -g` now recover a global package group whose `node_modules` directory was deleted [#​15093](https://github.com/pnpm/pnpm/issues/15093).
- `pnpm add -g` now installs local tarballs when `PNPM_HOME` contains `..` path segments [#​15118](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15044).
- `pnpm publish` now allows a detached Git HEAD in CI environments [#​5894](https://github.com/pnpm/pnpm/issues/5894).
- `pnpm store prune` now removes unreferenced files and packages from the content-addressable store [#​3635](https://github.com/pnpm/pnpm/issues/3635), as well as expired or superseded `pnpm dlx` cache data [#​15171](https://github.com/pnpm/pnpm/issues/15171).
- `pnpm cache list-registries` now prints decoded registry URLs [#​15046](https://github.com/pnpm/pnpm/issues/15046).
- `pnpm deploy` no longer triggers an install when running scripts in a read-only deployed filesystem [#​11617](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14786).
- `pnpm change check` now validates pending change intents in `.changeset/` [#​15183](https://github.com/pnpm/pnpm/issues/15183).
- `pnpm --filter` and `pnpm -F` shell completion now suggests workspace package names [#​15216](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15034).
- `pnpm --version` no longer creates a temporary file in the project directory during store detection [#​15264](https://github.com/pnpm/pnpm/issues/15264).
- `pnpm setup` now describes displayed configuration changes as "The following configuration changes were made" [#​15100](https://github.com/pnpm/pnpm/issues/15100).
- `minimumReleaseAge` approval prompts in `pnpm install` and `pnpm update -g` now count and display each package version once [#​15083](https://github.com/pnpm/pnpm/issues/15083), [#​15091](https://github.com/pnpm/pnpm/issues/15091).
- `.npmrc` authentication warnings now report when an empty environment variable removes an auth token and name the affected key [#​4806](https://github.com/pnpm/pnpm/issues/4806).
- The install summary now names the version each dependency resolved to when `node-linker` is `hoisted` [#​15161](https://github.com/pnpm/pnpm/issues/15161).
- `pnpm install` now re-links a package's global virtual store slot after `allowBuilds` changes [#​15117](https://github.com/pnpm/pnpm/issues/15117).
<!-- sponsors -->
##### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
##### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.5.1`](https://github.com/pnpm/pnpm/releases/tag/v12.5.1): pnpm 12.5.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.5.0...pnpm@12.5.1)
##### Patch Changes
- 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.
<!-- sponsors -->
##### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
##### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.5.0`](https://github.com/pnpm/pnpm/releases/tag/v12.5.0): pnpm 12.5
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.4.2...v12.5.0)
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.
##### Minor Changes
##### Installing packages
- `pnpm add` accepts a [Package URL](https://github.com/package-url/purl-spec) 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.
```yaml
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.
##### Configuring pnpm
- `supportedArchitectures` now accepts a list of platforms, in place of the `os`, `cpu` and `libc` axes.
```yaml
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.
```yaml
tasks:
test:rust:
concurrencyGroup: cargo
concurrencyGroups:
cargo: 2
```
- `tools` names the programs pnpm downloads, and `mirror` says where each one comes from.
```yaml
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`.
##### Python interpreters and environments
- `pnpm install` now chooses a Python interpreter for each project instead of installing every project with one interpreter [#​14945](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14945). The builds are [python-build-standalone](https://github.com/astral-sh/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](https://github.com/pnpm/pnpm/issues/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.
##### Python projects and workspaces
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15015).
- Python projects can now select extras and dependency groups through `[tool.pnpm.python]` in `pyproject.toml` [#​14945](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14945). Projects with only a `requirements.txt` file now get a Python environment and lockfile.
##### Python dependencies and lockfiles
- 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](https://github.com/pnpm/pnpm/issues/14945).
```yaml
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](https://github.com/pnpm/pnpm/issues/14945). pnpm reads uv's own overrides and constraints from `pyproject.toml` too.
- `pnpm install` now supports Python dependencies from Git repositories [#​14945](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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.
##### Patch Changes
##### Installing packages
- pnpm no longer reuses one package's downloaded tarball for another package whose resolution pins a different integrity hash to the same URL [#​15021](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14880).
##### Cargo projects
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14960). The generated `Cargo.lock` now also includes the dependencies weak features reference, which Cargo rejected with `--locked` for crates such as `uuid` [#​14978](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14962).
##### Python projects
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14945).
##### Performance
- `pnpm audit` no longer hangs on dependency graphs with many shared dependencies [#​15005](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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.
##### Other commands
- `pnpm deploy` now links commands exposed by workspace dependencies into the deployed project's `node_modules/.bin` directory [#​14899](https://github.com/pnpm/pnpm/issues/14899).
- `pnpm dlx` and `pnx` now prompt to approve dependency build scripts in interactive terminals [#​14943](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14766).
- `pnpm outdated --long` fills the Details column with the package homepage again [#​14886](https://github.com/pnpm/pnpm/issues/14886).
<!-- sponsors -->
##### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
##### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.4.2`](https://github.com/pnpm/pnpm/releases/tag/v12.4.2): pnpm 12.4.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.4.1...v12.4.2)
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.
##### Patch Changes
##### Security
- Dependency executables can no longer take over another package's POSIX bin shim through its shell helpers. Reinstall dependencies to replace existing shims [#​14837](https://github.com/pnpm/pnpm/issues/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.
##### Installing packages
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14790).
- `pnpm install <pkg>` now accepts `--prod` and `--dev`, including `--prod=false` [#​14868](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14891).
##### Resolving and updating dependencies
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14800).
- `pnpm update` now settles the lockfile in one run when an upgrade removes the package providing an optional peer dependency [#​14895](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14836).
- `pnpm update --no-save` now succeeds under `minimumReleaseAgeStrict` when every resolved version is old enough [#​14835](https://github.com/pnpm/pnpm/issues/14835).
##### Performance
- 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](https://github.com/pnpm/pnpm/issues/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.
##### Python projects
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14910).
- `pnpm add pypi:...` now rejects unsupported `--save-prefix` values before editing the manifest or resolving dependencies.
##### Workspaces and scripts
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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.
##### CLI and output
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14831).
- `pnpm self-update` no longer reinstalls the active version when it was installed by the standalone installation script [#​14823](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14773).
<!-- sponsors -->
##### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
##### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.4.1`](https://github.com/pnpm/pnpm/releases/tag/v12.4.1): pnpm 12.4.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.4.0...v12.4.1)
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.
##### Patch Changes
##### Installing packages
- `pnpm install` no longer fails with `Operation not permitted` when the filesystem refuses a hard link or a copy-on-write clone [#​14722](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14777). Imports also failed with "Permission denied" on filesystems that deny hard links and reflinks, and now fall back to copying [#​14780](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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.
##### Resolving and linking dependencies
- `pnpm install`, `pnpm add`, and `pnpm dedupe` now apply `ignoredOptionalDependencies` [#​14729](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14714).
- `pnpm dedupe` now keeps a compatible auto-installed peer when another workspace project depends on a newer major [#​14697](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14770). pnpm reported these as unmet whatever version the linked workspace project supplied.
##### Performance
- Sped up repeat installs [#​14540](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14725). Each package still runs its prepare scripts in its own copy of the checkout.
##### Running scripts and tasks
- pnpm now passes Ctrl+C on to the script or command it started and waits for it to shut down [#​14723](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14692). Changing a symlinked input's target invalidates that task's cache, and `pnpm pipeline --no-cache` no longer hashes task inputs.
##### Commands
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13857).
- `pnpm sbom` now leaves out a package's author field when the manifest author name is empty or contains only whitespace [#​14685](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14684). The fractional seconds it carried were rejected by strict SPDX consumers.
##### Configuration
- The `updateConfig` pnpmfile hook now receives the resolved configuration, including settings that came from `.npmrc`, the command line, or a default [#​14676](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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.
##### Windows
- `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](https://github.com/pnpm/pnpm/issues/14682). A permanent permission error delayed the failure by a minute. Sharing and lock violations keep their one minute retry budget.
##### Messages and output
- pnpm now warns when the root `package.json` declares a non-empty `workspaces` array and the project has no `pnpm-workspace.yaml` [#​2255](https://github.com/pnpm/pnpm/issues/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.
<!-- sponsors -->
##### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
##### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.4.0`](https://github.com/pnpm/pnpm/releases/tag/v12.4.0): pnpm 12.4
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.3.4...v12.4.0)
##### Minor Changes
- 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.
- Add Python packages with `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](https://github.com/pnpm/pnpm/issues/14566).
- Add Rust crates with `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](https://github.com/pnpm/pnpm/issues/14431), [#​14597](https://github.com/pnpm/pnpm/issues/14597), [#​7582](https://github.com/pnpm/pnpm/issues/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.
##### Patch Changes
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14604).
- Sped up installs in workspaces with many projects when reusing a warm global virtual store [#​14540](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14539), [#​14671](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14590). Installs with JSR dependencies in the lockfile also no longer fail with `ERR_PNPM_META_FETCH_FAIL` [#​14649](https://github.com/pnpm/pnpm/issues/14649).
- Boolean flags now accept explicit inline values. For example, `pnpm install --prod=false` installs devDependencies, while `--prod=true` skips them [#​14553](https://github.com/pnpm/pnpm/issues/14553).
- `pnpm install <pkg>` now accepts `--offline` and `--prefer-offline`, as `pnpm add <pkg>` already did [#​14194](https://github.com/pnpm/pnpm/pull/14194).
- Fixed `pnpm install --frozen-lockfile` rejecting a freshly generated lockfile when overrides use relative `file:` or `link:` paths in a workspace [#​14555](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14633), [#​14575](https://github.com/pnpm/pnpm/issues/14575).
- `pnpm import` now respects `lockfileDir` and branch lockfiles without modifying other lockfiles. Failed imports restore the destination lockfile [#​14563](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14559), [#​14557](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14618).
- On Windows, installs now retry replacing command shims temporarily locked by another process [#​14549](https://github.com/pnpm/pnpm/issues/14549).
- Fixed argument forwarding on Windows with `shellEmulator` enabled. Paths ending in a backslash, line breaks, and literal shell expressions are preserved [#​14548](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14646).
- Installs now respect the archive extraction concurrency limit even after a download is abandoned [#​14585](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14591).
- Shell completions now support the `pn` alias in bash, fish, pwsh, and zsh [#​11955](https://github.com/pnpm/pnpm/issues/11955).
- `pnpm version` now accepts `-m` as a short alias for `--message` [#​14567](https://github.com/pnpm/pnpm/issues/14567).
<!-- sponsors -->
##### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
##### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.3.4`](https://github.com/pnpm/pnpm/releases/tag/v12.3.4): pnpm 12.3.4
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.3.3...v12.3.4)
#### Patch Changes
- Sped up dependency resolution in large workspaces [#​14352](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14346).
`pnpm remove` now accepts `--unsafe-perm`, the same flag `pnpm install`, `pnpm add`, and `pnpm update` take.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.3.3`](https://github.com/pnpm/pnpm/releases/tag/v12.3.3): pnpm 12.3.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.3.2...v12.3.3)
#### Patch Changes
- Fixed concurrent installs sharing a store occasionally failing with an ENOENT error while importing a package file [#​14353](https://github.com/pnpm/pnpm/issues/14353).
- Sped up writing the lockfile in large workspaces [#​14352](https://github.com/pnpm/pnpm/issues/14352).
- Sped up dependency resolution in large workspaces [#​14352](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14346).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.3.2`](https://github.com/pnpm/pnpm/releases/tag/v12.3.2): pnpm 12.3.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.3.1...v12.3.2)
#### Patch Changes
- `pnpm audit --fix update` no longer aborts when a vulnerable package has no safe version inside its declared range [#​14508](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14352).
- Sped up installs in large workspaces. The resolver and the peer pass allocate less for every dependency edge [#​14352](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14483).
- Sped up installs in large workspaces. Saving the lockfile is faster, and the install finishes without waiting for memory cleanup [#​14352](https://github.com/pnpm/pnpm/issues/14352).
- `pnpm install` now relinks workspace packages when `publishConfig.linkDirectory` changes. Frozen installs report an outdated lockfile until it is regenerated [pnpm/pnpm#14488](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14352).
- `pnpm install` now reports "Already up to date" when local tarball dependencies have not changed [#​14495](https://github.com/pnpm/pnpm/issues/14495).
- `pnpm update` now accepts `--ignore-scripts` and skips lifecycle scripts during the update [pnpm/pnpm#14512](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14510).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.3.1`](https://github.com/pnpm/pnpm/releases/tag/v12.3.1): pnpm 12.3.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.3.0...v12.3.1)
#### Patch Changes
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14352).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.3.0`](https://github.com/pnpm/pnpm/releases/tag/v12.3.0): pnpm 12.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.2.1...v12.3.0)
#### Minor Changes
- 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.
#### Patch Changes
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14449).
- `pnpm config` now accepts `-g`/`--global`, `--location`, and `--json` before its subcommand [pnpm/pnpm#14421](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14428).
- Sped up installs in large workspaces. `pnpm-lock.yaml` is now read while the workspace projects are being discovered [#​14352](https://github.com/pnpm/pnpm/issues/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 `--filter`ed project, `--workspace-concurrency=1`, a dependency chain, or a task declaring `concurrency: 1` [#​14397](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14352).
- Sped up dependency resolution in large workspaces that use `link:` dependencies [#​14352](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14469).
- Sped up dependency resolution in large workspaces. The resolver builds fewer lookup keys for each dependency [#​14352](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14437).
- Sped up installs in large workspaces. The workspace dependency graph is now built once per run instead of twice [#​14352](https://github.com/pnpm/pnpm/issues/14352).
- Sped up writing `pnpm-lock.yaml` in large workspaces [#​14352](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14417).
- Fixed `pnpm repo` and `pnpm docs` failing to open the Windows browser from WSL [pnpm/pnpm#14467](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14405).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.2.1`](https://github.com/pnpm/pnpm/releases/tag/v12.2.1): pnpm 12.2.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.2.0...v12.2.1)
#### Patch Changes
- 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.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.2.0`](https://github.com/pnpm/pnpm/releases/tag/v12.2.0): pnpm 12.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.1.0...v12.2.0)
#### Minor Changes
- Catalogs can now resolve workspace dependencies through the `workspace:` protocol.
#### Patch Changes
- 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](https://github.com/pnpm/pnpm/issues/13261). Fixed `pnpm audit --fix=override` ignoring the `saveExact` and `savePrefix` settings when writing vulnerability overrides [#​11523](https://github.com/pnpm/pnpm/issues/11523).
- Authenticate Node.js runtime downloads from `nodeDownloadMirrors` with URL-scoped npm registry credentials, including bearer tokens, basic auth, and `tokenHelper` [pnpm/pnpm#14334](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14353)).
- Fixed the PowerShell shim generated by `npm install -g pnpm` on Windows so it invokes the native `pnpm.exe` binary [pnpm/pnpm#14362](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14347).
- Fixed `catalog:` ranges in workspace package peer dependencies being reported as unmet [pnpm/pnpm#14361](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/orgs/pnpm/discussions/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14349).
- Fixed command-line `--side-effects-cache` overrides being ignored when `pnpm-workspace.yaml` uses the object form of `sideEffectsCache` [pnpm/pnpm#14338](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14383).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.1.0`](https://github.com/pnpm/pnpm/releases/tag/v12.1.0): pnpm 12.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v12.0.0...v12.1.0)
#### Minor Changes
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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:
```yaml
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:
```yaml
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](https://github.com/pnpm/rfcs/pull/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:
```yaml
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:
- A dependency cycle among the tasks of a run is now an error naming the participating tasks (`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.
- Under `--no-bail`, tasks whose dependencies failed are reported as skipped, not failed, and do not add to the exit code.
- With `--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.
- Output is inherited rather than piped only when at most one script can ever be in flight (`--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](https://github.com/pnpm/pnpm/issues/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.
#### Patch Changes
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14294).
- Fixed `pnpm doctor` reporting a version that does not match `pnpm --version` [pnpm/pnpm#14225](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14301).
- Fixed automatically switched pnpm versions forcing all descendant pnpm processes to use the same version [pnpm/pnpm#14309](https://github.com/pnpm/pnpm/issues/14309).
- Fixed `pnpm deploy --prod` failing when an excluded dev dependency was also declared as an optional peer dependency [pnpm/pnpm#14302](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14270).
`pnpm update -g` also no longer changes the pnpm version. pnpm's own global install belongs to `pnpm self-update` [#​14270](https://github.com/pnpm/pnpm/issues/14270).
- When multiple versions of the same package expose the same binary, pnpm now links the binary from the highest version [#​14249](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14257).
- Colored output is no longer printed as raw escape sequences in the Windows Command Prompt [#​14292](https://github.com/pnpm/pnpm/issues/14292). Commands such as `pnpm list` now style their output there.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v12.0.0`](https://github.com/pnpm/pnpm/releases/tag/v12.0.0): pnpm 12
[Compare Source](https://github.com/pnpm/pnpm/compare/pnpm@11.28.1...v12.0.0)
#### Major Changes
- 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:
```sh
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](https://github.com/pnpm/pnpm/issues/13846), and a peer-resolution verdict computed for one occurrence of a cyclic package could be wrongly reused at another [#​13865](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13286).
- `pnpm install --frozen-lockfile false` is no longer supported. Use `pnpm install --no-frozen-lockfile` to disable frozen-lockfile mode. To enable it, use `pnpm install --frozen-lockfile` without a separate `true` argument [#​14741](https://github.com/pnpm/pnpm/issues/14741).
#### Minor Changes
- 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:
- A git-hosted dependency is prepared with the package manager it asks for. Its `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](https://github.com/pnpm/pnpm/issues/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.
#### Patch Changes
- 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](https://github.com/pnpm/pnpm/issues/13525).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
<!-- Migration note for pnpm/pnpm#14741 written by an agent (Codex, GPT-6). -->
### [`v11.28.1`](https://github.com/pnpm/pnpm/releases/tag/v11.28.1): pnpm 11.28.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.28.0...pnpm@11.28.1)
pnpm 11.28.1 makes `pnpm install` work in StackBlitz WebContainers, repairs lockfiles whose patch hashes disagree with `patchedDependencies`, and fixes many bugs in Windows command shims, injected workspace dependencies, and `pnpm deploy`.
##### Patch Changes
##### Installing packages
- `pnpm install` now works in StackBlitz WebContainers. On projects without a lockfile, it used to fail with `ENOENT ... pnpm-lock.yaml`, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls. It also failed with "this.db.exec is not a function", because `node:sqlite` there lacks `DatabaseSync.exec`. When `node:sqlite` cannot prepare statements either, pnpm stores the index in `index.fallback` [#​15649](https://github.com/pnpm/pnpm/issues/15649).
- `pnpm install` now completes after downloading a Node.js runtime specified by `devEngines.runtime` when pnpm runs on Node.js 24.4.x [#​14667](https://github.com/pnpm/pnpm/issues/14667).
- `pnpm install` no longer fails when a package from the registry declares a `file:` dependency on a directory inside itself, such as `"@types/css-tree": "file:./typings/css-tree"`. pnpm links that dependency to the directory inside the package, as npm and Yarn do. The lockfile records it as `link:<root>/typings/css-tree` [#​9141](https://github.com/pnpm/pnpm/issues/9141).
- Installing a git-hosted dependency that has to be built no longer fails when that dependency's own dependencies have build scripts nobody approved. pnpm skips those builds while preparing the dependency, as it does without `strictDepBuilds` [#​9764](https://github.com/pnpm/pnpm/issues/9764).
- pnpm now uses pnpm to prepare a git-hosted dependency that is a pnpm workspace without a committed lockfile. It used npm before, which could skip the dependency's build [#​14011](https://github.com/pnpm/pnpm/issues/14011).
- When installing a git dependency over SSH fails with `Permission denied (publickey)`, pnpm suggests checking the loaded keys with `ssh-add -l`. Resolving an SSH URL that refuses the key also shows a local HTTPS rewrite that leaves the recorded URL alone [#​13743](https://github.com/pnpm/pnpm/issues/13743).
- `pnpm install --dev` and `pnpm fetch --dev` now install the optional dependencies of devDependencies, such as the platform binaries of Biome and oxlint. The project's own `optionalDependencies` are still skipped [#​9678](https://github.com/pnpm/pnpm/issues/9678).
- `pnpm install --frozen-lockfile` now works on a detached HEAD when `gitBranchLockfile` is enabled. The install now reads the lockfiles of the local and remote-tracking branches that contain the checked-out commit. It still writes the shared `pnpm-lock.yaml` [#​7672](https://github.com/pnpm/pnpm/issues/7672).
- `pnpm install` on CI now fails on an outdated lockfile when `preferFrozenLockfile` is explicitly set to `true`. Setting it to `true` used to let CI update the lockfile [#​9072](https://github.com/pnpm/pnpm/pull/9072).
- `pnpm install` now fails with `ERR_PNPM_IGNORED_BUILDS` on a repeat install when `strictDepBuilds` is on and a dependency's build is still undecided. A repeat install against an existing `node_modules` reported success where a fresh install failed [#​10450](https://github.com/pnpm/pnpm/issues/10450).
- `pnpm install` now removes an optional dependency from `node_modules` if its install script fails. Code that checks whether the package is installed no longer finds a package that cannot load [#​8756](https://github.com/pnpm/pnpm/issues/8756).
- `pnpm install --offline` and `pnpm add --offline` now resolve a version range to the newest matching version whose tarball is already in the store. They used to pick the newest version in the cached metadata and fail with `ERR_PNPM_NO_OFFLINE_TARBALL` when its tarball was missing [#​10715](https://github.com/pnpm/pnpm/issues/10715).
- If an offline install fails because the registry metadata cache uses the layout from before pnpm 11.27 and 12.4, the error now names the older mirror on disk and explains that one online install repopulates the cache [#​15656](https://github.com/pnpm/pnpm/issues/15656).
- `pnpm install` now fails right away when writing package files fails because the store is full. It no longer retries the tarball download first [#​8581](https://github.com/pnpm/pnpm/issues/8581).
- With `nodeLinker: hoisted`, `pnpm install` now restores a workspace project's `node_modules` after it was deleted. Before, the install printed "Already up to date" and left the project without the dependencies nested under it.
- Under `nodeLinker: hoisted`, `pnpm install` now clears orphaned package directories that an interrupted or failed install leaves in a project's `node_modules`. A directory recorded by the previous install is removed, while an unrecorded directory is moved to `node_modules/.ignored`. A copy already in `.ignored` is never overwritten [#​13676](https://github.com/pnpm/pnpm/issues/13676).
- Packages in an external `virtualStoreDir` can resolve the project's direct dependencies selected by `hoistPattern`. Run `pnpm install --force` to repair an existing installation [#​5652](https://github.com/pnpm/pnpm/issues/5652).
- A repeat install now keeps the fast path when a declared local file dependency is replaced by an override [#​12892](https://github.com/pnpm/pnpm/issues/12892).
##### Store, build cache, and global virtual store
- 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](https://github.com/pnpm/pnpm/issues/3807).
- With the global virtual store, `pnpm rebuild` no longer modifies packages shared with projects that have not approved their build scripts [#​12302](https://github.com/pnpm/pnpm/issues/12302).
- The side-effects cache now restores the symlinks that a build script creates inside a package. A warm install used to replace each of them with a copy of its target [#​12859](https://github.com/pnpm/pnpm/issues/12859).
After upgrading, every package with a build script is built once more.
- `pnpm install` now restores cached build artifacts when reinstalling a workspace that uses separate lockfiles [#​12942](https://github.com/pnpm/pnpm/issues/12942).
- The global virtual store and the side-effects cache now key built packages by the Node.js version that the root project's `devEngines.runtime` or `engines.runtime` pins. That is the Node.js their build scripts run with. A dependency that declares its own `engines.runtime` no longer changes the key for every other package.
- Concurrent installs that share a global virtual store now run a package's build in its shared slot one at a time. A failed build leaves the slot in place and marks it for the next install to rebuild [#​15568](https://github.com/pnpm/pnpm/issues/15568).
- Concurrent installs no longer fail when they replace the same stale hoisted dependency link. Virtual store cleanup now keeps the temporary lockfiles that concurrent installs are still writing.
- `pnpm install` keeps the owner, group, and mode of files already in a shared store, including `index.db`. New store files and directories inherit the store directory's group-write bit. When that directory is setgid, new files inherit its group. pnpm does not change a file's owner or group [#​12765](https://github.com/pnpm/pnpm/issues/12765).
- When `pnpm install` repairs a store file that was modified through a hard link in `node_modules`, the repair now keeps the file's inode on Linux and macOS. Hard-linked copies in other projects are healed at the same time. Before, only the project running the install received the restored content. On Windows the repair still replaces the file, so other projects are healed on their next install [#​3445](https://github.com/pnpm/pnpm/issues/3445).
- A tarball whose integrity pnpm computed during download is now found in the store on the next install. Before, that install downloaded the tarball again once the lockfile recorded the integrity [#​12562](https://github.com/pnpm/pnpm/issues/12562).
- pnpm now warns when it cannot hard link packages from an existing store in the pnpm home directory and uses a store on the project's filesystem. This can happen when the project is on another filesystem, such as a bind-mounted workspace in a container. The warning names both stores and suggests setting `storeDir` [#​14505](https://github.com/pnpm/pnpm/issues/14505).
##### Resolving and linking dependencies
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13989).
- pnpm no longer reports unmet peer dependency warnings for aliased `npm:` peer ranges that a tarball dependency satisfies [#​11126](https://github.com/pnpm/pnpm/issues/11126).
- `pnpm install` now links the executables of auto-installed peer dependencies into the workspace root's `node_modules/.bin`, including after a frozen-lockfile reinstall [#​8511](https://github.com/pnpm/pnpm/issues/8511).
- With `resolutionMode: time-based` and `minimumReleaseAge` both set, `pnpm install` no longer reports a subdependency as too new when only the time-based cutoff excludes it. Such subdependencies used to fail a strict install with `ERR_PNPM_NO_MATURE_MATCHING_VERSION`, or were added to `minimumReleaseAgeExclude` [#​13569](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/16298).
- With `minimumReleaseAge` set, re-resolving the lockfile no longer rewrites the `peerDependencies` recorded for a package whose version did not change. This happened when the registry metadata of a package differed from the `package.json` in its tarball [#​13988](https://github.com/pnpm/pnpm/issues/13988).
- pnpm no longer revalidates cached registry metadata when the registry sends `Cache-Control: max-age=0`, `no-cache`, or `no-store`. It downloads the metadata again, so a version newly published to such a registry is visible on the next install [#​13487](https://github.com/pnpm/pnpm/issues/13487).
- pnpm's built-in package compatibility database no longer applies to a project's own manifest. A project named like a published package, such as `vue-loader`, no longer gains dependencies on `pnpm install` or `pnpm update`. User-configured `packageExtensions` still apply to project manifests [#​11700](https://github.com/pnpm/pnpm/issues/11700).
- Relative local tarball paths in `pnpm.overrides` without an explicit `file:` prefix are now rebased correctly for workspace packages [#​11131](https://github.com/pnpm/pnpm/issues/11131).
- pnpm no longer reports `pnpm-lock.yaml` as broken when a project depends on a package named `constructor`. A `__proto__` key in the lockfile is now kept as a plain entry when pnpm reads or writes the lockfile. It no longer replaces the prototype of the objects pnpm builds from it [#​11028](https://github.com/pnpm/pnpm/issues/11028).
- With `nodeLinker: pnp`, a workspace package can now require another workspace package it depends on [#​3567](https://github.com/pnpm/pnpm/issues/3567). On Windows, workspace dependency paths in the generated `.pnp.cjs` now use forward slashes.
##### Performance
- pnpm now uses less memory when installing a package whose archive is larger than 64 MiB unpacked, and when installing a runtime from a zip archive, such as Node.js on Windows, Deno, or Bun [#​14164](https://github.com/pnpm/pnpm/issues/14164).
- A fresh install reusing a warm global virtual store skips reimporting packages whose target directory is already complete [#​11112](https://github.com/pnpm/pnpm/issues/11112).
- A warm `pnpm install` reuses on-disk package metadata for five minutes when the registry does not send an ETag. Registries that send an ETag, including the public npm registry, still revalidate with a conditional request. `pnpm update` still fetches current metadata [#​13976](https://github.com/pnpm/pnpm/issues/13976).
##### Patched dependencies
- `pnpm install` now repairs a `pnpm-lock.yaml` whose `(patch_hash=<hash>)` dependency paths disagree with its `patchedDependencies` map, including paths that lack the hash their patch calls for. pnpm previously accepted such a lockfile as up to date and kept the old patched files. `pnpm install --frozen-lockfile` now fails on such a lockfile with `ERR_PNPM_INCONSISTENT_PATCH_HASH`. It fails with `ERR_PNPM_UNCHECKABLE_PATCH_HASH` when a patch hash in the lockfile is malformed, or when the lockfile lacks the package version or patch entry that the check needs [#​15336](https://github.com/pnpm/pnpm/pull/15336).
- `pnpm install` now fails with `ERR_PNPM_PATCH_NOT_FOUND` when a patch file listed in `patchedDependencies` does not exist. It used to fail with a raw `ENOENT` error and a stack trace [#​5268](https://github.com/pnpm/pnpm/issues/5268).
- pnpm now fails with `ERR_PNPM_INVALID_PATCHED_DEPENDENCY` when `patchedDependencies` has an invalid shape or contains a non-string value.
- `engineStrict` now checks the patched `package.json` when a `patchedDependencies` entry changes `engines`. A patch that relaxes `engines.node` no longer fails the install against the published range [#​9603](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/9699).
- `pnpm patch-commit` now fails with an error when `git` cannot be found in `PATH`. It previously reported that no changes were found [#​8666](https://github.com/pnpm/pnpm/issues/8666).
##### Workspaces and filtering
- `pnpm install` refreshes injected copies of workspace packages when source projects are rebuilt. Injected copies previously stayed stale until `pnpm install --force` [#​4407](https://github.com/pnpm/pnpm/issues/4407).
- Scripts listed in `syncInjectedDepsAfterScripts` now update injected dependencies while they run. A watcher on the injected package, such as a dev server, sees each change before the script exits [#​4410](https://github.com/pnpm/pnpm/issues/4410).
- `pnpm install` no longer fails for an injected workspace dependency whose package publishes from a `publishConfig.directory` that its own `prepare` script builds. The injected copy now picks up that directory once `prepare` finishes building it. `pnpm install --frozen-lockfile` no longer reports the dependency as outdated while the directory has not been built yet [#​7811](https://github.com/pnpm/pnpm/issues/7811).
- With `sharedWorkspaceLockfile: false`, an injected workspace package that has lifecycle scripts is now hard linked into the projects that depend on it. Before, pnpm left a plain copy, so later edits to the package did not reach those projects [#​9828](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/10446).
- `pnpm install` no longer creates a `node_modules` symlink inside the `publishConfig.directory` of a workspace package linked with `linkDirectory`. A build tool that cleaned its output directory through that symlink deleted the files of the package's dependencies. `pnpm install` also removes a symlink that an earlier install left there [#​16226](https://github.com/pnpm/pnpm/issues/16226).
- When `verifyDepsBeforeRun` triggers an install before a filtered `pnpm run` or `pnpm exec`, pnpm now installs only the selected projects and their dependencies. A later filtered command also installs a selected project that an earlier filtered install skipped [#​11865](https://github.com/pnpm/pnpm/issues/11865).
- `pnpm import` in a workspace now keeps the versions pinned by a `yarn.lock` inside a workspace project [#​4385](https://github.com/pnpm/pnpm/issues/4385).
- Installing with `pnprServer` set now records the pnpmfile checksum in the lockfile, so a later `pnpm install --frozen-lockfile` accepts that lockfile. A frozen install through the pnpr server now fails if the pnpmfile changed. If the pnpmfile defines a `readPackage`, `afterAllResolved` or `preResolution` hook or custom resolvers, pnpm resolves dependencies locally and prints a warning that the pnpr server was not used [#​14460](https://github.com/pnpm/pnpm/issues/14460).
Installing through a pnpr server now links a workspace project at the directory its `publishConfig.directory` names. An install that resolves through a server which does not forward the setting fails with `ERR_PNPM_PNPR_PUBLISH_DIRECTORY_MISMATCH`. The server rejects a `publishConfig.directory` that points outside its project.
##### Adding, updating, and removing dependencies
- With `autoInstallPeers`, `pnpm add` and `pnpm remove` in a workspace project keep the locked version of a peer dependency the project declares. In a workspace where another project depended on a different version of that package, the peer could switch to that version [#​11225](https://github.com/pnpm/pnpm/issues/11225).
- Adding a dependency now keeps unrelated transitive dependencies on their locked versions [#​11456](https://github.com/pnpm/pnpm/issues/11456).
- `pnpm update --recursive <pkg>` no longer changes the version of a peer dependency that another workspace project installs automatically. Such a peer could move to a version outside the range the project declares, for example to React 19 in a project that declares `react: ^18.3.1` [#​14928](https://github.com/pnpm/pnpm/issues/14928).
- `pnpm update package@beta` on a dependency declared as `catalog:` now updates the catalog entry and keeps `catalog:` in `package.json`. Before, pnpm wrote the resolved specifier to `package.json` [#​13399](https://github.com/pnpm/pnpm/issues/13399).
- `pnpm update` now applies an override that references a catalog with the catalog's new value when the update bumps that catalog entry. Before, the packages the override targets kept the old version in the lockfile [#​12159](https://github.com/pnpm/pnpm/issues/12159).
- `pnpm add <dir>` now warns when the added directory declares peer dependencies, as `pnpm link` does. The directory is saved as a `link:` dependency, and its peers are not resolved from the project that adds it. Use the `file:` protocol to have them resolved [#​5523](https://github.com/pnpm/pnpm/issues/5523).
##### Running scripts and commands
- `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](https://github.com/pnpm/pnpm/issues/16278).
- `pnpm run` no longer reinstalls dependencies when a `node_modules` directory installed outside CI is used with `CI=true`, or the other way around [#​12337](https://github.com/pnpm/pnpm/issues/12337).
- pnpm no longer hangs after a lifecycle script exits while a process it started in the background keeps the script's output open. pnpm stops reading that output one second after the script exits [#​5730](https://github.com/pnpm/pnpm/issues/5730).
- A signal sent to pnpm, such as `SIGTERM`, now reaches the pnpm that pnpm switches to because of `packageManager` or `devEngines.packageManager`, and the one that `pnpm with` runs. The signal used to be dropped, so scripts running under that pnpm never got to shut down [#​9948](https://github.com/pnpm/pnpm/issues/9948).
- `pnpm run` exits with the code of a script that handles Ctrl+C and shuts down. A script that finished cleanly is not reported as a lifecycle failure. The commands after it in the same script still run [#​9945](https://github.com/pnpm/pnpm/issues/9945).
- `pnpm run` and lifecycle scripts use the configured `scriptShell`, including Git Bash on Windows, when `shellEmulator` is also enabled. `shellEmulator` still runs scripts when `scriptShell` is not set. Extra arguments passed to `pnpm run` are quoted for the shell that runs the script, so a Windows path stays intact [#​14719](https://github.com/pnpm/pnpm/issues/14719).
- `pnpm -r run /regexp/` now honors the `tasks` `dependsOn` declared for each script the selector matches, like running the script by name does. Matched scripts that depend on each other run in order. Each matched script runs once [#​15596](https://github.com/pnpm/pnpm/issues/15596).
- Commands run from a POSIX shell through a dependency's own `node_modules/.bin`, such as `node_modules/vite/node_modules/.bin/esbuild`, no longer fail with `MODULE_NOT_FOUND` [#​10189](https://github.com/pnpm/pnpm/issues/10189).
- `pnpm rebuild` with `nodeLinker: hoisted` no longer puts one package's parent `node_modules/.bin` directories on the `PATH` of the packages it builds after it.
##### Publishing, packing, and deploying
- `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](https://github.com/pnpm/pnpm/issues/11454).
- `pnpm deploy` with a shared lockfile now copies workspace dependencies into the deploy directory, even when `packageImportMethod` is set to `hardlink`. Previously, their files were hard-linked to the workspace sources, so editing a source file also changed the deployed copy [#​12176](https://github.com/pnpm/pnpm/issues/12176).
- `pnpm deploy --prod` no longer fails with `ERR_PNPM_OUTDATED_LOCKFILE` when the deployed project declares a `devEngines.runtime` with `onFail: download`. The runtime stays out of the deployed `node_modules` with the rest of the dev dependencies [#​15703](https://github.com/pnpm/pnpm/issues/15703).
- `pnpm deploy --legacy` no longer leaves broken links to nested local dependencies of workspace packages [#​9575](https://github.com/pnpm/pnpm/issues/9575).
- `pnpm deploy` no longer creates extra directories inside the deploy target and workspace projects when using a relative deploy path [#​10981](https://github.com/pnpm/pnpm/issues/10981).
##### Manifests and configuration files
- Settings given on the command line, such as `--registry` and `--store-dir`, now take precedence over the values a pnpmfile `updateConfig` hook sets [#​14063](https://github.com/pnpm/pnpm/issues/14063).
- An `updateConfig` hook that returns `registriesByScope` without the `default` or `@jsr` entry no longer crashes the install with `Invalid URL`. A missing `default` keeps the configured `registry`, and a missing `@jsr` falls back to the built-in JSR registry [#​15619](https://github.com/pnpm/pnpm/issues/15619).
The hook's `registry` and the `default` entry of its `registriesByScope` now set one default registry, which installs, `pnpm publish`, and `pnpm login` all use. If a hook changes both, `registry` wins. A route that is not a string fails with `ERR_PNPM_INVALID_UPDATE_CONFIG_RESULT`.
- An `updateConfig` hook in `.pnpmfile.cjs` has to use `config.registriesByScope`, `config.registriesByPrefix`, and `config.registryOptionsByUrl`. These registry lookups were renamed in pnpm 11.23.0 from `config.registries`, `config.namedRegistries`, and `config.registryOptions` [#​15620](https://github.com/pnpm/pnpm/issues/15620).
- An async `updateConfig` hook that resolves to `undefined` now fails with `ERR_PNPM_CONFIG_IS_UNDEFINED`, as a synchronous hook that returns `undefined` already did.
- `pnpm config set --location=project` and `pnpm config delete --location=project`, run from a package inside a workspace, now write settings that belong in `pnpm-workspace.yaml` to the workspace root's `pnpm-workspace.yaml`. Before, they created a new `pnpm-workspace.yaml` in the current package, which made that package the workspace root. Settings stored in `.npmrc` are still written to the current directory [#​13757](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/16275).
- A `${VAR}` placeholder in `.npmrc` or `pnpm-workspace.yaml` whose name matches a built-in object property, such as `${toString}`, is now treated as an unset variable. It used to be replaced with the source text of a JavaScript function.
- pnpm now rejects a falsy non-array `packages` field in `pnpm-workspace.yaml`, such as `packages: false`, with an error. It used to treat the field as omitted.
##### Global packages, pnpm versions, and runtimes
- `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](https://github.com/pnpm/pnpm/issues/11528).
- `pnpm env remove --global` deletes Node.js versions that pnpm installed into its own store, including when another tool installed pnpm [#​8357](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/12006).
- `@pnpm/exe` no longer ships a binary for arm64 musl Linux, such as Alpine on ARM. The published binary crashed with a segmentation fault at startup. Installing `@pnpm/exe` on that platform now fails with an error that suggests `npm install -g pnpm` or pnpm 12 [#​10443](https://github.com/pnpm/pnpm/issues/10443).
- The macOS and Linux release archives no longer include Windows-only files, such as `node-gyp.cmd` and the `fastlist` executables [#​11352](https://github.com/pnpm/pnpm/issues/11352).
##### Windows and WSL
- 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](https://github.com/pnpm/pnpm/issues/16257).
- On Windows, command shims now run tools whose paths contain non-ASCII characters [#​6999](https://github.com/pnpm/pnpm/issues/6999). The PowerShell shims do so in Windows PowerShell 5.1 too [#​16217](https://github.com/pnpm/pnpm/issues/16217).
- On Windows, the `.cmd` command shims in `node_modules/.bin` now keep a `%` in the project path. Before, cmd.exe expanded it as a variable reference, so the command received a mangled `NODE_PATH` [#​15716](https://github.com/pnpm/pnpm/issues/15716).
- Bin shims in `node_modules/.bin` run from Cygwin on Windows again. The shims passed a `/cygdrive/c/...` path to the Windows `node` found on `PATH`, so Node.js failed with `Cannot find module 'C:\cygdrive\c\...'` [#​12845](https://github.com/pnpm/pnpm/issues/12845).
- `pnpm setup` no longer writes the `pn.ps1`, `pnpx.ps1`, and `pnx.ps1` PowerShell wrappers. It also removes the ones an earlier setup wrote. PowerShell now runs `pn`, `pnpx`, and `pnx` through their `.cmd` wrappers, like `pnpm` itself. Before, these aliases failed with a "not digitally signed" error wherever the execution policy blocks unsigned scripts [#​8444](https://github.com/pnpm/pnpm/issues/8444).
- On Windows, globally installed `@pnpm/exe` commands now run in the invoking PowerShell console and return their exit status [#​6503](https://github.com/pnpm/pnpm/issues/6503).
- On Windows, installing `@pnpm/exe` with npm inside a project now writes `node_modules/.bin` shims that run the standalone executable [#​15688](https://github.com/pnpm/pnpm/issues/15688).
- On Windows, `pnpm env use -g` and `pnpm add -g node@runtime:<version>` now replace a `node.exe` in the global bin directory that is a broken symlink. Previously they failed with `ENOENT` [#​5411](https://github.com/pnpm/pnpm/issues/5411).
- On Windows, pnpm expands nested `%VAR%` references in `PNPM_HOME` and the other directory environment variables it uses for its home, store, cache, state, and config directories. pnpm fails with an error when a `%VAR%` reference remains after expansion [#​13236](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/5283).
- On Windows, `pnpm setup` no longer garbles non-ASCII characters in existing `Path` entries [#​6346](https://github.com/pnpm/pnpm/issues/6346).
- On Windows, `pnpm setup` repairs the `PNPM_HOME` registry type left by older pnpm versions, even when the configured directory has not changed.
- On Windows, the `ERR_PNPM_BAD_ENV_FOUND` error of `pnpm setup` now shows the value `PNPM_HOME` is currently set to. It used to show the directory pnpm wanted to set.
- On Windows, `pnpm install` no longer skips a dependency's build script on a later install when the script changes nothing inside the package directory [#​15667](https://github.com/pnpm/pnpm/issues/15667).
- On Windows, pnpm now retries writing the workspace state file while another process, such as an antivirus scanner, briefly holds it open [#​14550](https://github.com/pnpm/pnpm/issues/14550).
##### Inspecting dependencies
- `pnpm licenses list` now reports the actual on-disk package locations when using `nodeLinker: hoisted` or `shamefully-hoist: true` [#​8589](https://github.com/pnpm/pnpm/issues/8589). `pnpm licenses list --json` now includes every installed copy of a package in its `paths` array, including hoisted copies and isolated installations with different peer dependencies. Its paths also exist on disk when the isolated linker uses a custom `modulesDir`.
- `pnpm list` now shows the correct path of a `link:` dependency that points to a directory on another drive on Windows. The path used to be appended to the project directory, such as `C:\project\D:\lib`, and `pnpm list --long` could not show the package's details [#​10362](https://github.com/pnpm/pnpm/issues/10362).
- `pnpm root` now prints the configured `modulesDir`. It used to print `node_modules` regardless of the setting. A project's own `modulesDir` from `packageConfigs` is printed too [#​9113](https://github.com/pnpm/pnpm/issues/9113).
- `pnpm audit` and `pnpm audit signatures` now fail with an error when the lockfile contains unresolvable dependency references [#​13638](https://github.com/pnpm/pnpm/issues/13638).
##### Output and messages
- With the default and append-only reporters, installs with `--loglevel warn` or `--loglevel error` now print the full output of a failed install script. The output of successful scripts, including the root project's own install hooks, stays hidden. With `--loglevel warn`, pnpm also prints ignored build script warnings.
- The lockfile verification error now suggests relaxing the policy that flagged an entry only if a fresh resolution still fails and you trust the affected packages. Errors from checks that no policy controls, such as a missing tarball integrity, no longer suggest relaxing a policy [#​14411](https://github.com/pnpm/pnpm/issues/14411).
- `pnpm run --recursive` now prints GitLab CI collapsible sections that GitLab recognizes. The section markers used to appear as raw text in the job log.
- Local tarball dependencies using the file protocol are no longer counted as downloaded in the progress banner [#​1103](https://github.com/pnpm/pnpm/issues/1103).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.28.0`](https://github.com/pnpm/pnpm/releases/tag/v11.28.0): pnpm 11.28
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.27.1...v11.28.0)
pnpm 11.28.0 adds the `forceIgnoresPlatform` setting and `pnpm update --peer`, and fixes many bugs in `pnpm deploy`, `--filter`, `nodeLinker: hoisted`, and custom `modulesDir` setups. This release also carries security fixes for shell completion, bin shims on Nix, lifecycle scripts inside a custom `modulesDir`, and `userAgent` placeholders in `pnpm-workspace.yaml`.
##### Minor Changes
- Added the `forceIgnoresPlatform` setting. When it is `false`, `pnpm install --force` skips optional dependencies whose `os`, `cpu` or `libc` do not match the host instead of installing all of them. The default stays `true` [#​6133](https://github.com/pnpm/pnpm/issues/6133).
##### Patch Changes
##### Security
- 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](https://github.com/pnpm/pnpm/issues/15415).
- pnpm no longer treats packages inside a custom `modulesDir` as workspace projects, including one that `packageConfigs` sets for a project. Before, with a `modulesDir` such as `vendor` and a `packages` pattern such as `**`, a repeat install ran the lifecycle scripts of dependencies that `allowBuilds` had not approved [#​15412](https://github.com/pnpm/pnpm/pull/15412).
- 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](https://github.com/pnpm/pnpm/issues/14883).
- Shell completion now omits candidates containing control or invisible formatting characters, and fish completion omits names containing backslashes. Package and script names can no longer inject extra completion records or terminal escape sequences.
- Commands that run pnpm again, such as `pnpm runtime set` and `pnpm env use`, no longer re-run a script that only looks like pnpm. A script named `pnpm` or `pn` that another package installed was run as though it were pnpm.
- `pnpm store prune` now leaves a `dlx` cache root that is a symlink or Windows junction untouched. Cleanup no longer removes directories through that link.
##### Installing packages
- `pnpm install` now fails at once when a registry or tarball server presents a TLS certificate that fails verification, such as a self-signed or expired one. The error names the certificate problem. Such requests were retried for more than a minute [#​9134](https://github.com/pnpm/pnpm/issues/9134).
- `pnpm install` no longer appears to hang when a git dependency is fetched over SSH and ssh asks for a key passphrase or a host key confirmation. pnpm now runs ssh in batch mode, so the install fails right away with the ssh error, and a key that needs a passphrase has to be loaded into an SSH agent first. An ssh command selected through `GIT_SSH_COMMAND`, `GIT_SSH`, or the `core.sshCommand` git setting is kept as is [#​2227](https://github.com/pnpm/pnpm/issues/2227).
- pnpm no longer crashes on startup when the temporary directory set by `TMPDIR`, `TEMP`, or `TMP` does not exist [#​4960](https://github.com/pnpm/pnpm/issues/4960).
- `pnpm install --silent` no longer fails when the install is delegated to pacquet. pnpm also stops passing `-s`, `--loglevel` and the other reporting flags to pacquet [#​11936](https://github.com/pnpm/pnpm/issues/11936).
- Fixed `pnpm install` failing with `EEXIST` when a concurrent install cleared the file or directory that was occupying a symlink path. On Windows, a symlink another process is still holding is no longer moved aside and recreated.
- `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](https://github.com/pnpm/pnpm/issues/14550).
- Installing or adding dependencies no longer fails when a previously installed local tarball file was deleted from disk [#​8367](https://github.com/pnpm/pnpm/issues/8367).
- `pnpm install` now reads the same local tarball it installs when a dependency's absolute `file:` path contains `..`. Such a path could install a different tarball than the one it read, failing with `ERR_PNPM_TARBALL_INTEGRITY`, or fail to resolve at all.
- `pnpm install --frozen-lockfile` now rejects changed local tarballs, even when the previous archive contents are in the store [#​1889](https://github.com/pnpm/pnpm/issues/1889).
- `pnpm add` and `pnpm install` now support installing bzip2 compressed tarballs [#​6761](https://github.com/pnpm/pnpm/issues/6761).
- `pnpm install` now fetches committed submodules of git dependencies [#​1470](https://github.com/pnpm/pnpm/issues/1470).
- Interrupting `pnpm install` with Ctrl+C or SIGTERM no longer leaves a temporary lockfile (`.pnpm-lock.yaml.*.tmp`) behind in the project [#​1418](https://github.com/pnpm/pnpm/issues/1418).
- `pnpm install` and `pnpm run` now reinstall a single project that was moved or renamed together with its `node_modules`. Before, they reported "Already up to date" while links such as Windows junctions still pointed at the old location [#​9512](https://github.com/pnpm/pnpm/issues/9512).
- `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](https://github.com/pnpm/pnpm/issues/9758).
- `pnpm install` and `pnpm add` no longer skip optional dependencies that the Node.js version resolved for a `devEngines.runtime` range supports, when the range uses `onFail: download`. An explicitly set `nodeVersion` still takes priority [#​14628](https://github.com/pnpm/pnpm/issues/14628).
- `pnpm install` now uses the running Node.js when `devEngines.runtime` declares a range without `onFail: download`. Optional dependencies supported by the active Node.js are no longer skipped [#​15230](https://github.com/pnpm/pnpm/issues/15230).
- `pnpm install --engine-strict` now respects `engines` relaxed by `readPackage` hooks in `.pnpmfile.cjs` [#​15482](https://github.com/pnpm/pnpm/issues/15482).
- `pnpm install` now applies changes to or removal of a global `readPackage` hook when an existing lockfile is present [#​15136](https://github.com/pnpm/pnpm/issues/15136).
- The project's `.pnpmfile.mjs` or `.pnpmfile.cjs` now runs after the pnpmfiles of config dependency plugins [#​9891](https://github.com/pnpm/pnpm/issues/9891).
- A `readPackage` hook that sets a dependency range to a value other than a string, such as `undefined`, now fails the install with an error that names the dependency, the package, and the pnpmfile. Delete the property to remove a dependency [#​5517](https://github.com/pnpm/pnpm/issues/5517).
- `pnpm install --prod` and other installs that skip `devDependencies` no longer run the `pnpm:devPreinstall` script [#​7065](https://github.com/pnpm/pnpm/issues/7065). They skip `prepare` lifecycle scripts too, as does `pnpm install` given package arguments. `pnpm deploy` does not run the `prepare` scripts of the deployed project [#​7282](https://github.com/pnpm/pnpm/issues/7282).
- The root project's `preinstall` script now runs before dependencies are resolved and linked. A guard such as `npx only-allow yarn` can stop the install before pnpm populates `node_modules` [#​3760](https://github.com/pnpm/pnpm/issues/3760).
- `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](https://github.com/pnpm/pnpm/issues/15344).
- `pnpm prune --prod` and production installs now prune excluded development dependencies even when lockfile generation is disabled.
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/11574).
- 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](https://github.com/pnpm/pnpm/issues/14833).
##### Resolving and linking dependencies
- 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](https://github.com/pnpm/pnpm/issues/8912).
- Removal overrides such as `"parent>peer": "-"` now prevent optional peers from being installed from another workspace package [#​15008](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15007).
- Trim leading and trailing whitespace from dependency override selectors in `pnpm.overrides` [#​6356](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14176).
- `pnpm install` and `pnpm update` now resolve a dependency range to the newest matching version that is not deprecated. A version already recorded in the lockfile is still used [#​15128](https://github.com/pnpm/pnpm/issues/15128).
- `pnpm add` and `pnpm remove` no longer move unrelated transitive dependencies to other versions. Adding a package and then removing it now leaves `pnpm-lock.yaml` unchanged. Before, the dependencies of auto-installed peers and `npm:` aliased subdependencies could move to a newer version that was already in the lockfile [#​11859](https://github.com/pnpm/pnpm/issues/11859).
- `pnpm dedupe` now moves transitive dependencies to the version a `catalog:` dependency pins, as it already did for versions written directly in `package.json`.
- `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](https://github.com/pnpm/pnpm/issues/10944).
- `pnpm install --frozen-lockfile` now succeeds when an optional dependency was unresolvable and skipped by the install that wrote the lockfile. Previously, frozen installs failed with `ERR_PNPM_OUTDATED_LOCKFILE`. The notice states that the dependency could not be resolved and names the requested range [#​3960](https://github.com/pnpm/pnpm/issues/3960).
- pnpm no longer rewrites `packageManagerDependencies` in `pnpm-lock.yaml` when that block pins `@pnpm/exe` beside `pnpm`. The rewrite ran on every command, so `pnpm list` left a clean working tree dirty, and `pnpm version` then refused to run [#​14926](https://github.com/pnpm/pnpm/issues/14926).
- `pnpm import` and fresh resolutions now record `integrity` for git-hosted tarballs, such as `codeload.github.com` URLs, even when the tarball is already in the store [#​13338](https://github.com/pnpm/pnpm/issues/13338).
- Merging lockfiles now preserves recorded configuration fields such as `overrides`, `neverBuiltDependencies`, `patchedDependencies`, `packageExtensionsChecksum`, `settings`, and `catalogs` [#​8366](https://github.com/pnpm/pnpm/issues/8366).
- A lockfile entry whose resolution is unchanged now keeps its recorded `deprecated` message [#​5772](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/4518).
- 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](https://github.com/pnpm/pnpm/issues/7553).
- Workspace projects that `hoistPattern` or `publicHoistPattern` selects are now hoisted on every install. A project added to the workspace was not hoisted until `node_modules` was deleted and reinstalled. A workspace that installs nothing from a registry hoisted none of its projects at all [#​3642](https://github.com/pnpm/pnpm/issues/3642).
- 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](https://github.com/pnpm/pnpm/issues/7568).
- `pnpm install` now links a dependency's bin even when the bin's file does not exist yet, such as a workspace package's bin that a build script creates after install. Previously pnpm printed a `Failed to create bin` warning. The command then stayed missing until `node_modules` was removed [#​10007](https://github.com/pnpm/pnpm/issues/10007), [#​10216](https://github.com/pnpm/pnpm/issues/10216).
- Executable linking now makes a bin executable for every user. A target with only some execute permission bits set was left non-executable for other users [#​3699](https://github.com/pnpm/pnpm/issues/3699). Bin linking leaves workspace and linked dependency files outside `node_modules` unchanged.
- Dependencies and executable binaries are now correctly linked and accessible for workspace packages using `publishConfig.directory` and `publishConfig.linkDirectory` [#​8338](https://github.com/pnpm/pnpm/issues/8338).
##### Workspaces and filtering
- Fixed `pnpm install` for workspace projects reached through a symlink, such as a `packages` directory that links to a folder outside the workspace. pnpm now installs their dependencies, and the links in their `node_modules` resolve [#​1044](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/6483). A workspace project with build metadata is no longer skipped when it matches the requested range and has the same version precedence as the registry package [#​2812](https://github.com/pnpm/pnpm/issues/2812).
- Secondary dependencies now prefer the version resolved by the local project's direct dependencies over versions from sibling workspace projects [#​7191](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/7667).
- `pnpm install --frozen-lockfile` now fails when a workspace package's version no longer satisfies the range that a dependent workspace project declares for it. This includes injected workspace dependencies [#​7823](https://github.com/pnpm/pnpm/issues/7823).
- `pnpm install -r` now installs every workspace project when `recursiveInstall` is set to `false` in `pnpm-workspace.yaml` [#​7504](https://github.com/pnpm/pnpm/issues/7504).
- `pnpm install` with `--filter` now installs only the dependencies of the selected projects when using `nodeLinker: hoisted` [#​8882](https://github.com/pnpm/pnpm/issues/8882).
- `pnpm install` now updates an injected workspace dependency after that package's own dependencies change, when `shared-workspace-lockfile` is `false` [#​7209](https://github.com/pnpm/pnpm/issues/7209).
- `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](https://github.com/pnpm/pnpm/issues/14703).
- Fixed injected workspace dependency synchronization failing with `EPERM` on Windows when removing nested directories.
- Workspace discovery now returns one project per directory when multiple manifest formats are present. It selects `package.json`, then `package.json5`, then `package.yaml` [#​3027](https://github.com/pnpm/pnpm/issues/3027).
- Wildcards in negated `packages` patterns of `pnpm-workspace.yaml` now match directories whose names start with a dot. For example, `!packages/**` now also excludes `packages/.dev/tool` when another pattern includes `.dev` explicitly.
- 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](https://github.com/pnpm/pnpm/issues/11724).
- pnpm now warns when `shared-workspace-lockfile` is passed on the command line outside a workspace [#​1617](https://github.com/pnpm/pnpm/issues/1617).
- 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](https://github.com/pnpm/pnpm/issues/9907).
- `--filter "[<since>]"` now selects workspace packages when dependency versions change in a catalog in `pnpm-workspace.yaml` [#​8718](https://github.com/pnpm/pnpm/issues/8718). It also selects projects that files were moved out of when git detects the move as a rename [#​15481](https://github.com/pnpm/pnpm/issues/15481).
- `--filter` now evaluates selectors in order, so later inclusion filters can re-include packages that an earlier exclusion filter excluded [#​9354](https://github.com/pnpm/pnpm/issues/9354).
- Directory filters such as `--filter=./packages/*` now select projects when the current directory was entered with a lowercase drive letter on Windows, like `c:\repo` [#​5500](https://github.com/pnpm/pnpm/issues/5500).
##### Custom modulesDir
- `pnpm run`, `pnpm exec`, `pnpm version` hooks, and the lifecycle scripts a project runs during install now find commands in a custom `modulesDir`, and bin shims resolve dependency paths there [#​3604](https://github.com/pnpm/pnpm/issues/3604). Project `.hooks` scripts are read from the configured modules directory. Installs resolved by pnpr now preserve configured modules and executable directories. `pnpm bin` now reports the configured executable directory. In a workspace whose projects keep their own lockfiles, a `packageConfigs` entry that gives one project its own `modulesDir` is followed too.
- Tools installed in a custom `modulesDir` can load CommonJS plugins installed there, the same way they would from `node_modules`. When executables are symlinks, as with `preferSymlinkedExecutables` or the hoisted linker, this works for `pnpm run`, `pnpm exec`, `pnpm version` hooks, and the lifecycle scripts a project runs during install. A symlinked tool started directly from a shell does not get it. Paths containing the platform path-list separator do not receive this fallback. `extendNodePath: false` disables this fallback [#​3604](https://github.com/pnpm/pnpm/issues/3604).
- 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.
##### Adding, updating, and removing dependencies
- `pnpm add` now keeps the specifier a `readPackage` hook provides when the hook rewrites the requested one. When the hook removes the dependency instead, the add skips it and reports why. Previously the add wrote the request either way, and the hook undid it on the next read, so `pnpm install --frozen-lockfile` failed [#​15156](https://github.com/pnpm/pnpm/issues/15156).
- `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](https://github.com/pnpm/pnpm/issues/8627).
- `pnpm add` now saves the requested exact version when adding a dependency, even when the manifest already contains a version range [#​6040](https://github.com/pnpm/pnpm/issues/6040).
- `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](https://github.com/pnpm/pnpm/issues/5096).
- Added a `--peer` flag to `pnpm update` to update ranges in `peerDependencies` [#​8081](https://github.com/pnpm/pnpm/issues/8081).
- `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](https://github.com/pnpm/pnpm/issues/6714).
- `pnpm update <name>` now updates a peer dependency that pnpm installed automatically [#​10486](https://github.com/pnpm/pnpm/issues/10486).
- `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](https://github.com/pnpm/pnpm/issues/9953).
- `pnpm update --prod` no longer installs devDependencies when run in a project installed with `--prod` [#​8038](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13923).
- `pnpm remove` now runs the project's own `preuninstall`, `uninstall`, and `postuninstall` scripts. `preuninstall` and `uninstall` run before dependencies are unlinked. A failure in either stage aborts the removal. `postuninstall` runs after unlinking completes. The `ignoreScripts` setting and `--lockfile-only` skip all three stages [#​3276](https://github.com/pnpm/pnpm/issues/3276).
- `pnpm remove -r` now fails if any requested dependency is absent from all selected workspace projects. Validation respects `--save-prod`, `--save-dev`, and `--save-optional` and completes before modifying project manifests [#​2319](https://github.com/pnpm/pnpm/issues/2319).
- `pnpm remove` now accepts `--trust-lockfile` and `--no-trust-lockfile` to control supply-chain policy checks while removing a package [#​14406](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/4219).
- `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](https://github.com/pnpm/pnpm/issues/14612).
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/4385).
- `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](https://github.com/pnpm/pnpm/issues/6866).
- `pnpm patch-commit` now falls back to copying package files when hard linking fails.
- `make-dedicated-lockfile` no longer removes fields such as `main` and `types` from the `publishConfig` of the project's `package.json`. It now restores `package.json` when it cannot move the original `node_modules` back to its place. The error then names `.tmp_node_modules`, where the original `node_modules` was left. The command refuses to run while that directory exists, so a retry cannot overwrite it.
##### Running scripts and commands
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/10151).
- `pnpm exec` and `pnpm dlx` now set `npm_execpath`, `INIT_CWD`, `npm_node_execpath`, and `NODE` for child processes [#​7037](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/1550).
- A script that runs `pnpm run` no longer adds duplicate `node_modules/.bin` and `node-gyp-bin` entries to `PATH` [#​5352](https://github.com/pnpm/pnpm/issues/5352).
- Concurrent `pnpm run` and `pnpm exec` commands now serialize their dependency installs [#​14551](https://github.com/pnpm/pnpm/issues/14551).
- `pnpm run --recursive` no longer reports interrupted scripts as lifecycle failures after `Ctrl+C`.
- `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](https://github.com/pnpm/pnpm/issues/4750).
- `pnpm install-test` now accepts `--no-bail` when executed directly and in recursive runs [#​3777](https://github.com/pnpm/pnpm/issues/3777).
- `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](https://github.com/pnpm/pnpm/issues/8611).
- 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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/7562).
- If a script is killed by a signal that pnpm survives, such as SIGPIPE, the error now names the signal: `Command failed with signal SIGPIPE.` [#​9821](https://github.com/pnpm/pnpm/issues/9821).
##### Publishing, packing, and deploying
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/8208).
- `pnpm pack` now honors the `files` field of `package.yaml` and `package.json5` manifests. Git-hosted and injected local dependencies that use these manifests now honor it too [#​7906](https://github.com/pnpm/pnpm/issues/7906). The archive includes exactly one `package.json` when the project uses an alternative manifest format, even when `.npmignore` or `files` excludes the source file.
- `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](https://github.com/pnpm/pnpm/issues/4164).
- `pnpm publish` and `pnpm pack` now report an error when a bin script has a shebang line ending with CRLF [#​7311](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/8787).
- `pnpm deploy` now respects `--package-import-method` passed on the command line and reports the package import method correctly [#​7593](https://github.com/pnpm/pnpm/issues/7593).
- `pnpm deploy` no longer triggers an install when running scripts in a read-only deployed filesystem [#​11617](https://github.com/pnpm/pnpm/issues/11617).
- A legacy `pnpm deploy` with `node-linker=hoisted` now puts the deployed project's direct dependencies at the top of the deployed `node_modules` [#​9671](https://github.com/pnpm/pnpm/issues/9671).
- `pnpm deploy --legacy` no longer rewrites `node_modules/.pnpm-workspace-state-v1.json` in the source workspace. The next `verifyDepsBeforeRun` check there reported the workspace as out of date [#​15352](https://github.com/pnpm/pnpm/issues/15352).
##### Manifests and configuration files
- 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 now preserves CRLF line endings when it modifies project manifests.
- `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](https://github.com/pnpm/pnpm/issues/13736), [#​13519](https://github.com/pnpm/pnpm/issues/13519).
- `pnpm change check` now validates the pending change intents in `.changeset/`. It fails when an intent names a package that is not in the workspace or cannot be released.
- `.npmrc` 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](https://github.com/pnpm/pnpm/issues/14404).
- pnpm now expands environment variables in `_auth.authToken` values loaded from global `config.yaml` and `pnpm_config__auth` [#​12828](https://github.com/pnpm/pnpm/issues/12828).
- pnpm now warns when an empty environment variable removes an `.npmrc` authentication token. Authentication environment warnings now name the affected key [#​4806](https://github.com/pnpm/pnpm/issues/4806).
- 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](https://github.com/pnpm/pnpm/issues/15530).
- pnpm now treats a missing global `config.yaml`, `auth.ini`, or other optional config file as absent in Node.js-compatible runtimes such as StackBlitz WebContainers. Commands such as `pnpm --version` failed there with `ENOENT` [#​14030](https://github.com/pnpm/pnpm/issues/14030).
- `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](https://github.com/pnpm/pnpm/issues/12055).
- `pnpm doctor` now checks the configured default registry and sends its credentials. It used to always ping `https://registry.npmjs.org/` [#​15618](https://github.com/pnpm/pnpm/issues/15618).
##### Global packages, pnpm versions, and runtimes
- 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](https://github.com/pnpm/pnpm/issues/14531).
- `pnpm add -g`, `pnpm update -g`, and `pnpm remove -g` now recover a global package group whose entire `node_modules` directory was deleted. `pnpm remove -g` leaves such a group's command shims in the global bin directory [#​15093](https://github.com/pnpm/pnpm/issues/15093). These commands 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](https://github.com/pnpm/pnpm/issues/12533).
- `pnpm update -g` no longer asks more than once for approval of the same immature `name@version` when `minimumReleaseAgeStrict` is enabled [#​15091](https://github.com/pnpm/pnpm/issues/15091).
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14709).
- `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](https://github.com/pnpm/pnpm/issues/7067).
- `pnpm env remove` now cleans up dangling Node.js executables and symlinks. 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](https://github.com/pnpm/pnpm/issues/7123).
##### Windows and WSL
- `pn`, `pnpx`, `pnx`, and `pnpm` now run when Git Bash, MSYS2, or Cygwin launches them through a Windows path such as `C:\Users\me\node_modules\pnpm\pn`. The aliases used to fail to find the pnpm installed beside them, or hand the call to an unrelated one [#​14884](https://github.com/pnpm/pnpm/issues/14884).
- Fallback `.cmd` and `.ps1` Windows wrappers in `@pnpm/exe` now propagate the exit status of the invoked `pnpm` command [#​14826](https://github.com/pnpm/pnpm/issues/14826).
- 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](https://github.com/pnpm/pnpm/issues/3360).
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/9461).
- On Windows, pnpm now fails within about a second when it cannot move a `node_modules` directory installed by another package manager because a file in it is in use. The error names the directory and suggests stopping the process that uses it. pnpm used to retry for a minute and then print a raw `EPERM` stack trace [#​7505](https://github.com/pnpm/pnpm/issues/7505).
- 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](https://github.com/pnpm/pnpm/issues/8101).
- `pnpm install` now resolves local tarballs specified with bare UNC paths on Windows [#​1669](https://github.com/pnpm/pnpm/issues/1669).
- pnpm now recognizes local paths with forward slashes on Windows.
- On Windows, `pnpm add` and `pnpm update` now write relative `file:` and `link:` specifiers with forward slashes to `package.json` and the lockfile. They used to write backslashes, so the same project produced different files on Windows and on other systems [#​7497](https://github.com/pnpm/pnpm/issues/7497), [#​9687](https://github.com/pnpm/pnpm/issues/9687).
- Fixed scripts failing with errors such as `'an-compile' is not recognized` when `scriptShell` is set to `cmd.exe` on Windows [#​7181](https://github.com/pnpm/pnpm/issues/7181).
##### Inspecting dependencies
- `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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/12200).
- `pnpm audit --fix=update` now fixes vulnerabilities in dependencies declared through an npm alias. A specifier such as `"foo": "npm:vulnerable-pkg@1.0.0"` moves to the patched version and keeps the alias. Versions pinned with a leading `=` are fixed as well [#​15155](https://github.com/pnpm/pnpm/issues/15155).
- `pnpm audit --fix` now prunes redundant overrides when one vulnerable range is a subset of another for the same package [#​8577](https://github.com/pnpm/pnpm/issues/8577).
- Running `pnpm list` inside a workspace package without `--recursive` or a filter now lists only the current package [#​14494](https://github.com/pnpm/pnpm/issues/14494). `pnpm licenses list` does the same. Use `--recursive` or `--filter` to list the licenses of other workspace projects [#​5689](https://github.com/pnpm/pnpm/issues/5689).
- `pnpm list --only-projects` now prints every project selected with `--filter` or `--recursive`, including a project that has no workspace dependencies [#​9770](https://github.com/pnpm/pnpm/issues/9770). It also lists the workspace projects when `sharedWorkspaceLockfile` is `false` [#​7151](https://github.com/pnpm/pnpm/issues/7151), and a project that sets `publishConfig.directory` [#​10635](https://github.com/pnpm/pnpm/issues/10635). It no longer reports packages in `node_modules` that are missing from the lockfile [#​9528](https://github.com/pnpm/pnpm/issues/9528).
- `pnpm licenses list` failed or reported nothing in a workspace with `sharedWorkspaceLockfile: false`. It now reads the lockfile of each selected project [#​10140](https://github.com/pnpm/pnpm/issues/10140).
- With `nodeLinker: hoisted`, `pnpm licenses list` reported paths under `node_modules/.pnpm` that do not exist. It now reports the directory where the hoisted linker placed each package [#​8589](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/7693).
- `pnpm sbom` filtered to a single workspace project no longer replaces the project's own `license` or `bugs` field with the workspace root's value when the project's value is blank. The same applies to an `author`, `description`, `license`, `repository`, or `bugs` field set to `null` [#​14882](https://github.com/pnpm/pnpm/issues/14882).
- `pnpm store status` no longer reports packages with build or postinstall scripts as modified in the store. When packages were mutated, it now lists only those packages and no longer suggests running `pnpm install --force` [#​919](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/15351).
- The `pnpm:peer-dependency-issues` log event, which `--reporter ndjson` prints, no longer lists peers silenced by `peerDependencyRules.ignoreMissing` under `conflicts` or `intersections` [#​8295](https://github.com/pnpm/pnpm/issues/8295).
##### Output and messages
- The error for an incompatible pnpm-lock.yaml now reports the lockfileVersion the file was generated with and the lockfileVersion the current pnpm supports. The error also warns that recreating the lockfile with `--force` may break the application and suggests installing the pnpm version that generated the lockfile [#​848](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/3646).
- Resolution no longer logs an error when a package metadata request fails and resolution succeeds via cached metadata [#​2522](https://github.com/pnpm/pnpm/issues/2522).
- The ignored build scripts warning and the update notice are printed as plain lines when output is not a terminal, in CI, or with `--reporter append-only`. They were drawn inside a box that broke apart in CI logs [#​9421](https://github.com/pnpm/pnpm/issues/9421).
- `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](https://github.com/pnpm/pnpm/issues/8944).
- `pnpm add` now warns when replacing an existing dependency with a specifier pointing to a different source [#​14869](https://github.com/pnpm/pnpm/issues/14869).
- `pnpm update` no longer warns "Skip adding ... to the default catalog" for a dependency that already uses `catalog:` [#​13715](https://github.com/pnpm/pnpm/issues/13715).
- `pnpm remove --help` no longer shows a `[@<version>]` suffix in its usage line. The command accepts package names only [#​7751](https://github.com/pnpm/pnpm/issues/7751).
- Bash completion now completes script names that contain a colon, such as `pnpm run test:u` to `pnpm run test:unit` [#​5482](https://github.com/pnpm/pnpm/issues/5482).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.27.1`](https://github.com/pnpm/pnpm/releases/tag/v11.27.1): pnpm 11.27.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.27.0...v11.27.1)
#### Patch Changes
- `pn`, `pnpx`, and `pnx` now run the pnpm installed alongside them. They used to look pnpm up on `PATH`. That failed when the directory holding them was not on `PATH`, and it silently handed the call to an unrelated pnpm when one came first there [#​14803](https://github.com/pnpm/pnpm/issues/14803).
- The `@zkochan/cmd-shim` package is now available as `@pnpm/bins.cmd-shim`.
- `pnpm cache list-registries` now prints the registry URL, matching `pnpm cache view`. It printed `https%3A+registry.npmjs.org` before and prints `https://registry.npmjs.org/` now [#​15046](https://github.com/pnpm/pnpm/issues/15046).
- `pnpm deploy` no longer installs the dependencies of the workspace root project into the deploy directory [#​6437](https://github.com/pnpm/pnpm/issues/6437).
- Install warnings no longer carry the text of a package's deprecation notice. The warning names the deprecated package and version, and the `pnpm:deprecation` event no longer carries the notice either. `pnpm view` still shows it on request.
A deprecation warning now names the newest version of the package that is not deprecated, and says when reaching it means widening the range you declared:
```
WARN deprecated foo@1.0.0. 2.3.1 is not deprecated, outside the range you declared.
```
pnpm works this out from the metadata it already fetched, so it costs no extra request. An install that reuses the lockfile without fetching metadata names no version.
pnpm strips control characters from the package name and version in a deprecation warning, and from the notice `pnpm outdated --long` prints.
The text sanitizer now also strips the Unicode line and paragraph separators U+2028 and U+2029.
- `pnpm exec <command>` and `pnpm <command>` run from a subdirectory of a project now find the executables installed in the project's `node_modules/.bin`. The command still runs in the subdirectory. `PNPM_PACKAGE_NAME` names the project [#​5068](https://github.com/pnpm/pnpm/issues/5068).
- `pnpm exec` and `pnpm dlx` now wait for the command to finish shutting down after `Ctrl+C`. A signal sent to pnpm alone now reaches the command, the way it does with `pnpm run`. pnpm used to exit on the interrupt and terminate the command while it was still shutting down [#​7374](https://github.com/pnpm/pnpm/issues/7374).
- Warnings about ignored environment variables in project `.npmrc` credentials now link to the npmrc documentation [pnpm/pnpm#15051](https://github.com/pnpm/pnpm/issues/15051).
- `pnpm audit --interactive --fix=update` no longer opens a second prompt for selecting dependencies to update [#​14927](https://github.com/pnpm/pnpm/issues/14927).
- Fixed `pnpm dedupe` requiring a second pass after bumping a direct dependency in `package.json` [pnpm/pnpm#14987](https://github.com/pnpm/pnpm/issues/14987).
- `pnpm deploy` now writes plain versions for registry dependencies with peer dependencies in the deployed `package.json`. The deployed lockfile retains the resolved peer bindings. npm aliases keep their target package names [#​14873](https://github.com/pnpm/pnpm/issues/14873).
- `pnpm publish` now allows a detached Git HEAD in CI, including checkouts of release tags. The working tree must still be clean. Branch and remote-history checks still apply when HEAD is attached [pnpm/pnpm#5894](https://github.com/pnpm/pnpm/issues/5894).
- `pnpm dlx` and `pnx` now prompt to approve dependency build scripts in interactive terminals. Cached packages with pending builds also prompt for approval. Without an interactive terminal, use `--allow-build` to allow the required builds. Fixes [pnpm/pnpm#14943](https://github.com/pnpm/pnpm/issues/14943).
- `pnpm install --force` now removes obsolete dependency links inside virtual-store packages when their dependencies change. Invalid dependency names are ignored during obsolete-link cleanup [#​15039](https://github.com/pnpm/pnpm/issues/15039).
- `pnpm add -g` and `pnpm update -g` now ignore incomplete unrelated global package groups when every command from the replaced group is retained. Operations that could remove a global command still require complete ownership information.
- Resolving a Node.js runtime now fails when unofficial-builds.nodejs.org cannot be reached. pnpm used to ignore that failure and leave the musl builds out of `pnpm-lock.yaml`. `pnpm update` then wrote a different lockfile on a machine whose network blocks the mirror [pnpm/pnpm#14813](https://github.com/pnpm/pnpm/issues/14813).
- pnpm now deduplicates a package whose child dependency resolved an optional peer in one workspace project but not in another. Two copies of `next` could appear when only some projects could reach `styled-jsx`'s optional `babel-plugin-macros` peer [#​14800](https://github.com/pnpm/pnpm/issues/14800).
- Fixed shell completion of package scripts for `pnpm run` and `pnpm run-script` [pnpm/pnpm#15034](https://github.com/pnpm/pnpm/issues/15034).
Bash completion now preserves literal script names containing glob characters and shell punctuation in pnpm v11 and v12.
- `pnpm sbom` now publishes a valid URL in the CycloneDX `externalReferences[].url` and the SPDX `homepage`. An npm shorthand such as `vercel/ms` or `gitlab:group/subgroup/project` is expanded to the `git+https` URL npm derives for it. An scp-style remote such as `git@github.com:vercel/ms.git` is expanded the same way. Any other URL is published in its normalized form, without embedded credentials. A value that names no repository, an email address for example, is left out. pnpm used to publish the raw value, so a shorthand produced a URL that consumers such as Dependency-Track reject [pnpm/pnpm#14773](https://github.com/pnpm/pnpm/issues/14773).
- `pnpm setup` now describes the displayed configuration changes as "the following configuration changes."
- `pnpm --version` now reports why the pnpm version a project pins cannot be installed or recorded, then prints the version of the running CLI. It used to fail, which made the command unusable where the filesystem is read-only. `pnpm --version` also honors `--store-dir` and its `--store` alias now [#​14831](https://github.com/pnpm/pnpm/issues/14831).
- `pnpm install --force` now reinstalls dependencies when the manifest and lockfile are unchanged. It previously reported "Already up to date" without reinstalling. Files changed in `node_modules` are restored when the store content is intact. Combining `--force` with `--frozen-store` now reports a configuration conflict on repeat installs [#​919](https://github.com/pnpm/pnpm/issues/919).
- `pnpm install` now installs git-hosted dependencies without preparing them when their builds are explicitly denied by `allowBuilds`. Dependencies that require preparation still need an explicit allow or deny decision [pnpm/pnpm#10522](https://github.com/pnpm/pnpm/issues/10522).
- `pnpm runtime set` and `pnpm env use` now use the pnpm version that started the command. They could run a different installed pnpm when the command was started through Corepack or another wrapper.
- The install summary now names the version each dependency resolved to when `node-linker` is `hoisted`. It also lists what an install restores after `node_modules` is deleted, and both sides of a version change. The summary showed the range recorded in `package.json`, or nothing at all [#​15161](https://github.com/pnpm/pnpm/issues/15161).
- The `@pnpm/npm-lifecycle` package is now available as `@pnpm/exec.npm-lifecycle`.
- Fixed `minimumReleaseAge` making pnpm download a package's full metadata again on every install. The cached copy carried a validator the registry could not match, so pnpm could never revalidate it [pnpm/pnpm#15103](https://github.com/pnpm/pnpm/issues/15103).
- pnpm now measures a `pnpm.overrides` entry written as a bare path, such as `./local-dep`, from the directory holding `pnpm-workspace.yaml`. It used to be measured from each package the override rewrote, so the dependency linked to a directory that does not exist [#​11131](https://github.com/pnpm/pnpm/issues/11131).
- pnpm now preserves scalar YAML anchors and aliases when editing `pnpm-workspace.yaml`. Removing the entry that defines an anchor keeps surviving aliases valid. Entries updated to different values are written separately [#​8245](https://github.com/pnpm/pnpm/issues/8245).
- pnpm now preserves comments and existing key order when updating `package.yaml`. New keys are appended to their mapping [pnpm/pnpm#2008](https://github.com/pnpm/pnpm/issues/2008).
- `pnpm install --prod` no longer downloads the registry packages that only a devDependency reaches [#​881](https://github.com/pnpm/pnpm/issues/881).
- `pnpm update --global` no longer reinstalls a global package when its dependency graph resolves to what is already installed. It reports `Already up to date` [pnpm/pnpm#12002](https://github.com/pnpm/pnpm/issues/12002).
- The `minimumReleaseAge` approval prompt now counts and displays each package version once [pnpm/pnpm#15083](https://github.com/pnpm/pnpm/issues/15083).
- `pnpm run` no longer sends a script a second `SIGINT` when `Ctrl+C` is pressed in a terminal. A script that shuts down on the first `SIGINT` and exits at once on a second used to die before its shutdown finished [#​7374](https://github.com/pnpm/pnpm/issues/7374).
- pnpm now reads a `pnpm-workspace.yaml` whose `tasks` section uses a setting only pnpm 12 acts on, such as `concurrencyGroup`. A task's unrecognized fields are ignored, unless the field only differs in case from `concurrency` or `dependsOn`, which pnpm reports as a typo.
The warning about unrecognized top-level settings now names `cargo`, `concurrencyGroups`, and `pipelines` as pnpm 12 settings.
- `pnpm -r list --json` now prints one JSON array. It printed a separate array for each project when `sharedWorkspaceLockfile` was `false`, so the output could not be parsed.
`pnpm -r list` now reads each project's own modules directory when the projects keep their own lockfiles, so `--long` and `--parseable` report the packages that project installed [#​15011](https://github.com/pnpm/pnpm/issues/15011).
- A signal sent to pnpm while it runs without a terminal, as a container runtime or a service manager does, now reaches the script even when the shell running it stays the script's parent. pnpm then waits for the script to finish shutting down. Such a signal used to end the shell at once or stay with it, and the script was never told to stop [#​7374](https://github.com/pnpm/pnpm/issues/7374).
- Fixed `minimumReleaseAge` being skipped for packages served by a registry that returns the same ETag for abbreviated and full package metadata [pnpm/pnpm#14925](https://github.com/pnpm/pnpm/issues/14925).
- `pnpm install` now returns "Already up to date" in a workspace where `dedupeDirectDeps` left a project without a `node_modules` directory of its own. Such a project forced a full install on every run.
- Installs in different projects that share a global virtual store no longer fail on Windows with `Access is denied` while repairing the same slot [#​15114](https://github.com/pnpm/pnpm/issues/15114).
- `pnpm sbom` now emits a license value as a CycloneDX expression only when it is a valid SPDX license expression. Anything else is emitted as a CycloneDX license name [pnpm/pnpm#14786](https://github.com/pnpm/pnpm/issues/14786).
- A dependency's own bins can no longer take over another package's bin shim. The POSIX shims pnpm generates used to look up their shell helpers on `PATH`, where a dependency's bins come first [#​14837](https://github.com/pnpm/pnpm/issues/14837). Reinstalling replaces the shims already in your `node_modules`. On Cygwin, MSYS2, and WSL the shims still take their Windows path conversion from `PATH`, so a dependency can still redirect them there.
- POSIX bin shims now convert a Windows-form path such as `C:\node_modules\.bin\tsc` correctly. The shim mangled the backslashes in such a path and could not reach the package it runs. Installing again replaces the shims already in `node_modules` [#​14867](https://github.com/pnpm/pnpm/issues/14867).
- `pnpm pack` now writes tarball entries grouped by file extension and file name, the order npm uses. Packages that ship many same-named files, such as template collections, pack much smaller [#​14766](https://github.com/pnpm/pnpm/issues/14766).
- A command run in a project that the workspace does not include now acts on that project alone. A project is outside the workspace when it has a manifest of its own and no pattern in the `packages` setting selects it, or when a `!` pattern excludes it. A directory with no manifest of its own, such as a package's source directory, still belongs to the workspace. `pnpm install` in an excluded project used to install every project in the workspace [#​3561](https://github.com/pnpm/pnpm/issues/3561).
- POSIX bin shims now take `cygpath` and `wslpath` from the system default path on Cygwin, MSYS2, and WSL2. The shims looked both helpers up on `PATH`, where a dependency's own bins come first, so a dependency could redirect another package's shim. Installing again replaces the shims already in `node_modules` [#​14866](https://github.com/pnpm/pnpm/issues/14866).
- `pnpm update` and `pnpm audit --fix=update` no longer copy dependencies added by `packageExtensions`, a `readPackage` hook, or an override into `package.json`. Those dependencies keep the specifier the hook or override gives them. `pnpm update --latest` no longer resolves past that specifier. `pnpm audit --fix=update` now warns when one of them pins a vulnerable version. The warning points at `pnpm audit --fix` [#​14928](https://github.com/pnpm/pnpm/issues/14928).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.27.0`](https://github.com/pnpm/pnpm/releases/tag/v11.27.0): pnpm 11.27
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.26.0...v11.27.0)
#### Minor Changes
- `nodeDownloadMirrors` can now be set in the global config file (`config.yaml`) and through the `PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS` environment variable, so a Node.js download mirror can be configured once for a machine instead of in every workspace [#​12124](https://github.com/pnpm/pnpm/issues/12124), [#​13611](https://github.com/pnpm/pnpm/issues/13611).
```sh
PNPM_CONFIG_NODE_DOWNLOAD_MIRRORS='{"release":"https://npmmirror.com/mirrors/node/"}'
```
- Added a new setting `trustPolicyExcludePrune` (default: `false`). When enabled, `pnpm add`, `pnpm update`, and `pnpm remove` prune the entries of `trustPolicyExclude` 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.
#### Patch Changes
- pnpm now reads the `packageManager`, `devEngines.packageManager` and runtime pins from the workspace root's `package.json` when `lockfileDir` is set. A project that moved its lockfile lost the pins it declared there [#​14633](https://github.com/pnpm/pnpm/issues/14633).
- Fixed `pnpm add -g`, `pnpm update -g`, and `pnpm remove -g` mutating global bins or install directories after only partially reading an installed package group. If any declared package manifest is missing, malformed, or unreadable, pnpm now fails before activation or removal and leaves the existing global installation intact [pnpm/pnpm#13796](https://github.com/pnpm/pnpm/issues/13796).
- `fetch-timeout` now limits how long a request may make no progress. The timer restarts on every chunk that arrives. A large download over a slow connection is no longer aborted while data is still coming in. A connection that stops delivering data still fails after `fetch-timeout` [#​14604](https://github.com/pnpm/pnpm/issues/14604).
- `pnpm peers check` no longer reports a peer dependency declared as `workspace:^`, `workspace:~`, or a bare `workspace:` as unmet. pnpm reported these as unmet whatever version the linked workspace project supplied [#​14770](https://github.com/pnpm/pnpm/issues/14770).
- A `readPackage` hook that edits its argument in place no longer changes what a later install in the same command resolves. A `deprecated` notice read from the lockfile no longer carries over to another install either [#​13988](https://github.com/pnpm/pnpm/issues/13988).
- `pnpm install` now auto-installs missing transitive peers when workspace projects share a dependency at different depths. This also removes incomplete duplicate peer contexts from the lockfile. Fixes [pnpm/pnpm#14840](https://github.com/pnpm/pnpm/issues/14840).
- GitHub Actions updates now stop if an action reference changes while its versions are being resolved. Unrelated workflow edits are preserved.
GitHub Actions homepage links no longer expose server credentials. GitHub server URLs now require HTTPS, with HTTP allowed only for loopback hosts.
- `pnpm licenses list` now reports the runtime downloaded through `devEngines.runtime` with `onFail: "download"`. The command previously failed with `ERR_PNPM_UNSUPPORTED_PACKAGE_TYPE` [#​14172](https://github.com/pnpm/pnpm/issues/14172).
- pnpm no longer creates a project `pnpm-lock.yaml` when `devEngines.packageManager.onFail` is `download` and lockfile writing is turned off with `lockfile: false` or `--no-lockfile`. pnpm still switches to the pinned version [#​14728](https://github.com/pnpm/pnpm/issues/14728).
- A `registry` or `@scope:registry` set in an `.npmrc` now wins over the registry a `pnpm login` credential stored in the global `config.yaml` points at. Previously, after logging in to one registry, installs in a project whose `.npmrc` named a private registry went to the logged-in registry instead. They now go to the registry the `.npmrc` names [#​14614](https://github.com/pnpm/pnpm/issues/14614).
- A patch that gives a dependency a `preinstall`, `install`, or `postinstall` script, or a `binding.gyp`, now runs that build. pnpm asks for build approval first, so the package is listed under "Ignored build scripts" until it is allowed to build. pnpm 12 ran nothing, and pnpm 11 ran it without asking [#​14648](https://github.com/pnpm/pnpm/issues/14648).
- Registries that share a host but differ by URL path — one JFrog Artifactory, Nexus, AWS CodeArtifact or GitLab Packages instance serving several repositories — now get a metadata cache directory each. Previously they shared one, so resolving a package from one of them could answer with another's versions, integrity hashes and tarball URLs and fail with `ERR_PNPM_TARBALL_URL_MISMATCH` [#​13558](https://github.com/pnpm/pnpm/issues/13558).
The URL scheme is part of the cache directory name too, so an `http` registry can no longer hand its metadata — which can be rewritten in transit — to a resolution configured for `https` at the same host.
The first install after upgrading refetches registry metadata once. The package store is untouched.
`pnpm cache view` now labels each entry with the full registry URL. It printed `registry.npmjs.org` before and prints `https://registry.npmjs.org/` now.
`pnpm cache list-registries` and `pnpm cache list` print the new directory names. Scripts that parse either command need updating.
- Updated the embedded Node.js release keys to the current canonical `nodejs/release-keys` list.
- `pnpm sbom` now omits package author fields when the manifest author name is empty or contains only whitespace [pnpm/pnpm#14685](https://github.com/pnpm/pnpm/issues/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`. The timestamp carried fractional seconds, which strict SPDX consumers rejected [#​14684](https://github.com/pnpm/pnpm/issues/14684).
- Windows filesystem operations now retry permission errors for up to one second. Permanent permission errors previously delayed failure by a minute. Sharing and lock violations retain their one-minute retry budget [pnpm/pnpm#14682](https://github.com/pnpm/pnpm/issues/14682).
- pnpm now writes `node_modules/.package-map.json` only when `nodeExperimentalPackageMap` is enabled. Nothing reads the file without that setting. An install that stops writing the map removes the one a previous install left.
- pnpm now unpacks a downloaded runtime archive into a randomly named directory inside the store. It previously used a predictable path, where another user of a shared store could plant a symlink and redirect the write outside the store ([GHSA-vwc7-r8mq-g2x9](https://github.com/advisories/GHSA-vwc7-r8mq-g2x9)).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.26.0`](https://github.com/pnpm/pnpm/releases/tag/v11.26.0): pnpm 11.26
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.25.0...v11.26.0)
#### Minor Changes
- Catalogs can now resolve workspace dependencies through the `workspace:` protocol.
- `pnpm remove` and `pnpm update` now accept `--trust-lockfile`, `--no-trust-lockfile`, `--trust-policy`, `--trust-policy-exclude`, and `--trust-policy-ignore-after`. `pnpm remove` checks the whole lockfile against the active policies unless `--trust-lockfile` is set.
- Added `pnpm change check` for CI validation of package versions against the `versioning.epics` bands and `versioning.fixed` groups in `pnpm-workspace.yaml`.
#### Patch Changes
- Fetch and tarball errors and retry logs now hide URL credentials, query strings, and fragments that could expose secrets.
- Fixed a race during config dependency updates that could redirect a lockfile write through a symlink [#​14322](https://github.com/pnpm/pnpm/issues/14322).
- `pnpm add --allow-build=!<pkg>` now correctly denies builds, including in global installs. `pnpm approve-builds <pkg>` and `pnpm approve-builds !<pkg>` now save decisions even when the package is not awaiting approval, with a warning [#​14067](https://github.com/pnpm/pnpm/issues/14067).
- Fixed `pnpm audit --fix` failing without a value or when followed by another flag. `pnpm audit --fix=override` now respects `saveExact` and `savePrefix` when writing overrides [#​13261](https://github.com/pnpm/pnpm/issues/13261), [#​11523](https://github.com/pnpm/pnpm/issues/11523).
- `pnpm audit` now excludes ignored advisories from vulnerability totals and severity counts, and reports them separately [#​14535](https://github.com/pnpm/pnpm/issues/14535).
- `pnpm deploy` no longer requires `injectWorkspacePackages`. If a workspace dependency's peer has multiple possible versions, deployment reports `ERR_PNPM_DEPLOY_AMBIGUOUS_PEER` with the conflicting versions. Pin the peer with `overrides` to deploy without injection [#​9386](https://github.com/pnpm/pnpm/issues/9386).
- Fixed concurrent installs sharing a store occasionally failing with an `ENOENT` error while importing a package file [#​14353](https://github.com/pnpm/pnpm/issues/14353).
- Fixed installation failures when a linked local dependency provides a peer dependency also provided by an ancestor, including with `pnpm deploy --legacy`.
- `pnpm install --node-linker=hoisted` no longer downloads skipped optional dependencies when `node_modules` already exists [#​14139](https://github.com/pnpm/pnpm/issues/14139).
- Fixed `pnpm install` rejecting a symlinked lockfile when config dependencies are unchanged. Updates to config dependencies also preserve lockfiles with a byte order mark. Writes through symlinked lockfiles remain blocked [#​14372](https://github.com/pnpm/pnpm/issues/14372).
- `pnpm install` now relinks workspace packages when `publishConfig.linkDirectory` changes. Frozen installs require the lockfile to be regenerated [#​14488](https://github.com/pnpm/pnpm/issues/14488).
- Auto-installed optional peers now satisfy their declared range even when the workspace root uses a version outside that range [#​13867](https://github.com/pnpm/pnpm/issues/13867).
- Fixed global virtual store paths for dependency cycles to consistently account for the runtime engine when dependencies have allowed builds [#​14341](https://github.com/pnpm/pnpm/issues/14341).
- Standalone installations now preserve the bundled `node-gyp` files needed to build native dependencies.
- Downloaded runtimes are now available to dependency lifecycle scripts during installation.
- Node.js downloads from `nodeDownloadMirrors` now use URL-scoped npm credentials, including bearer tokens, basic auth, and `tokenHelper` [#​14334](https://github.com/pnpm/pnpm/issues/14334).
- Fixed `globalDir` and `globalBinDir` handling in global configuration and environment variables, including `~/` expansion. This fixes `pnpm add -g` failing after `pnpm config set -g global-bin-dir` [#​14336](https://github.com/pnpm/pnpm/issues/14336).
- The JavaScript pnpm can again switch to the project's pinned pnpm version on hosts without a matching native binary. If the requested version requires an unavailable native binary, the error now identifies the unsupported host [#​13622](https://github.com/pnpm/pnpm/issues/13622).
- Global `pnpm config` commands now skip project package manager version switching, allowing authentication to be configured before downloading the pinned version [#​14463](https://github.com/pnpm/pnpm/issues/14463).
- `pnpm self-update`, `pnpm with`, and automatic version switching no longer wait through registry retries when a configured registry has no signatures and `registry.npmjs.org` is unavailable [#​14483](https://github.com/pnpm/pnpm/issues/14483).
- Fixed argument forwarding on Windows with `shellEmulator` enabled. Trailing backslashes, line breaks, and literal shell expressions are preserved [#​14548](https://github.com/pnpm/pnpm/issues/14548).
- Relative `scriptShell` paths now resolve from the workspace root. Bare command names such as `bash` still use `PATH` [#​14422](https://github.com/pnpm/pnpm/issues/14422).
- `pnpm import` now preserves the project-local lockfile when `lockfileDir` points elsewhere and restores the destination lockfile on failure. Branch lockfile imports leave the shared lockfile unchanged [#​14563](https://github.com/pnpm/pnpm/issues/14563).
- `catalogMode` and `--save-catalog` no longer move local paths, tarballs, or `workspace:<path>` specifiers into catalogs [#​14437](https://github.com/pnpm/pnpm/issues/14437).
- `--side-effects-cache`, `--no-side-effects-cache`, and `PNPM_CONFIG_SIDE_EFFECTS_CACHE` now toggle only the local cache, preserving any remote cache configured in `sideEffectsCache`.
- `pnpm unpublish` now handles registry two-factor authentication challenges through web authentication or a one-time password prompt [#​14464](https://github.com/pnpm/pnpm/issues/14464).
- `pnpm outdated` and `pnpm update` now follow GitHub Actions references using self-repository syntax, such as `uses: $/.github/actions/setup`.
- `pnpm remove` now accepts `--unsafe-perm`.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.25.0`](https://github.com/pnpm/pnpm/releases/tag/v11.25.0): pnpm 11.25
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.24.0...v11.25.0)
#### Minor Changes
- Added an opt-in proof of concept that lets installs reuse a dependency's build output across machines, by publishing and restoring signed, organization-scoped artifacts through pnpr instead of running the lifecycle scripts locally.
Configure it with the new `remoteSideEffectsCache` setting. A workspace names the eligible `organization` and `packages`; everything describing the act of signing — `publish`, `keyId`, `builderId`, `trustedKeys`, `privateKey` and the provenance fields — is refused in `pnpm-workspace.yaml` and read from the global config file or the environment instead.
- Added macOS and Windows x64 and arm64 support to remote shared build artifacts [pnpm/pnpm#13771](https://github.com/pnpm/pnpm/issues/13771).
- Added the `audit.ignorePrune` setting. When set to `true`, `pnpm audit --fix` removes ignored GHSA entries that no longer appear in the audit report.
- Generalized the experimental shared-artifact protocol so candidates and signed payloads identify a discriminated subject. Dependency side effects use package and source-integrity subjects, while workspace tasks use project and task subjects.
This changes shared-artifact request bodies and signed payloads. A pnpr server and its clients have to be on matching versions.
- `pnpm init` now pins the latest pnpm version, instead of the version of pnpm that ran the command. A project scaffolded by an outdated pnpm therefore no longer inherits that staleness through its own `devEngines.packageManager` / `packageManager` pin [#​7490](https://github.com/pnpm/pnpm/issues/7490).
The version is read from the `latest` tag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry, `offline`, or a `latest` that the `minimumReleaseAge` / `trustPolicy` settings reject — `pnpm init` pins the running version as before, and never fails or hangs on the lookup. A `latest` that is older than the running pnpm is never pinned either.
- A `scope` set in a project's `pnpm-workspace.yaml` is now ignored, with a warning naming where to set it instead. `pnpm login` records the scope as a `@scope:registry` route in the machine-global `auth.ini`, which outranks `~/.npmrc` in every project — so a repository-committed file could redirect a scope such as `@acme` for all of a user's other projects after one routine login. Use `--scope`, the `PNPM_CONFIG_SCOPE` environment variable, or the global config file instead [#​13557](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13771)).
- Persist completed recursive tasks so `--resume-from` skips exactly the work that passed during a matching interrupted or failed `pnpm -r run` / `pnpm -r exec` invocation. When no compatible state exists, pnpm retains its graph-based resume behavior.
- Allowed `pnpm update --patches` to refresh registry revisions through a configured pnpr server while retaining locked package versions.
- Added explicit registry revision selection with `<version>+rN` and `pnpm update --patches` for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries.
- Workspace install, rebuild, pack, publish, stage, and lifecycle work now starts as soon as its dependencies finish instead of waiting for an unrelated topological group.
- `pnpm stage approve` now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. The whole batch is approved with a single one-time password, and pnpm asks for a new one only once the registry stops accepting it. Inside a workspace, the selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry.
- Added per-task concurrency limits to workspace task orchestration. Set `tasks.<name>.concurrency` in `pnpm-workspace.yaml` to limit how many instances of that task may run across workspace projects at once:
```yaml
tasks:
build:
concurrency: 2
```
- Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the `registries` setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts.
- `sideEffectsCache` now declares the whole of how a package's build output is reused — whether one is restored, whether one is saved, and the remote tier that shares it between machines:
```yaml
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](https://github.com/pnpm/rfcs/pull/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:
```yaml
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:
- A dependency cycle among the tasks of a run is now an error naming the participating tasks (`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.
- Under `--no-bail`, tasks whose dependencies failed are reported as skipped, not failed, and do not add to the exit code.
- With `--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.
- Output is inherited rather than piped only when at most one script can ever be in flight (`--workspace-concurrency=1`, or the graph forces the scripts to run one after another).
#### Patch Changes
- An `_auth` entry in the global config file no longer decides which registry packages come from when something else says. A `registry` or `registries` declared in `pnpm-workspace.yaml` or the global config now wins over the route inferred from a stored credential, which still applies where nothing else declares one. The `pnpm_config__auth` environment variable is unchanged: it stays the way to point a CI runner at a mandated proxy, and still overrides what a repository declares.
- Prevent installs through a symlinked `node_modules` directory from rewriting the target checkout [pnpm/pnpm#14286](https://github.com/pnpm/pnpm/issues/14286).
- Treat empty scripts selected by a regular expression as missing before running dependent tasks.
- The options type of the `fetch` command now declares `allowBuilds`, a setting its handler already forwarded to the installer. Type-level only — what `pnpm fetch` does is unchanged.
- Filter hidden scripts matched by a regular expression during recursive runs when a visible script also matches.
- Fixed automatically switched pnpm versions forcing all descendant pnpm processes to use the same version [pnpm/pnpm#14309](https://github.com/pnpm/pnpm/issues/14309).
- Fixed `ERR_PNPM_UNUSED_PATCH` validation during incremental installs [pnpm/pnpm#13692](https://github.com/pnpm/pnpm/issues/13692).
- Fixed `pnpm deploy --prod` failing when an excluded dev dependency was also declared as an optional peer dependency [pnpm/pnpm#14302](https://github.com/pnpm/pnpm/issues/14302).
- `pnpm update -g` no longer downgrades a global package. `--latest` resolves the `latest` dist-tag, which can point at an older release than the one installed — after `pnpm add -g <pkg>@next`, for instance [#​14270](https://github.com/pnpm/pnpm/issues/14270).
`pnpm update -g` also no longer changes the pnpm version. pnpm's own global install belongs to `pnpm self-update` [#​14270](https://github.com/pnpm/pnpm/issues/14270).
- Copying a built package to its other hoisted locations no longer replaces the destination directory. With `nodeLinker: hoisted`, that replacement deleted the dependencies nested inside the destination's `node_modules`, and made concurrent copies of the same build chunk fail with `ERR_PNPM_ENOENT: no such file or directory, rename '.../node_modules/_tmp_...'` [#​12880](https://github.com/pnpm/pnpm/issues/12880).
- `pnpm update` no longer replaces the specifier a project declares for a dependency that is also listed in `overrides`. A `catalog:` reference stays a `catalog:` reference, and a declared range stays as written, instead of being rewritten to the version the override resolved to [#​12115](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/14224).
- Make `pnpm add --lockfile-only` skip dependency linking [pnpm/pnpm#14286](https://github.com/pnpm/pnpm/issues/14286).
- `--production` is accepted again as an alias of `--prod` on `install`, `fetch`, `prune`, `update`, `list`, `why`, and `sbom`, and the install that `verifyDepsBeforeRun` reproduces is now spelled with `--prod`. `pnpm run` no longer aborts with "unexpected argument '--production' found" after a production-only install [#​14147](https://github.com/pnpm/pnpm/issues/14147).
- The progress output no longer overwrites the lines above it once it grows taller than the terminal window [#​14270](https://github.com/pnpm/pnpm/issues/14270).
- Restoring a dependency's build from the remote side-effects cache no longer downloads files the store already holds.
- Forward `patchedDependencies` hashes and `packageExtensions` to pnpr so server-side resolution preserves patches and package extensions in the lockfile and installed packages.
- Published the workspace task graph and scheduler as `@pnpm/workspace.task-scheduler` so other workspace commands can use the same dependency-aware scheduling as recursive run and exec.
- The environment variables for the remote side-effects cache are named for the setting they configure: `PNPM_SIDE_EFFECTS_CACHE_REMOTE_KEY_ID`, `..._BUILDER_ID`, `..._IMAGE_DIGEST`, `..._ARCHITECTURE_BASELINE`, `..._PRIVATE_KEY`, `..._BUILD_ENV`, `..._TRUSTED_KEYS` and `..._PUBLISH`. The `PNPM_REMOTE_SIDE_EFFECTS_CACHE_*` names keep working, and the new one wins when both are set.
- A `devEngines.packageManager` range pin on pnpm is now recorded in `pnpm-lock.yaml`'s `packageManagerDependencies` when the running pnpm already satisfies it, using the running version and keeping the range as the recorded specifier. Previously only an exact pin — or a range resolved on the way through a version switch — reached the lockfile, so a range pin written by hand (or by any tool other than `pnpm add` / `pnpm self-update`) left the project without the shared resolution the pin exists to provide.
- Fixed recursive `run` cleanup on Windows when a lifecycle script fails while another script's process tree is still running.
- The update notification now suggests `pnpm self-update` when `PNPM_HOME` manages the pnpm in use, and the [standalone install script](https://pnpm.io/installation) otherwise — under Corepack, or when another package manager installed pnpm. `pnpm self-update` under Corepack names the standalone install script too.
- Enforce `allowBuilds` when a prepared git dependency is reused from the shared store, and use the lockfile's canonical git resolution ID in approval suggestions.
- Topologically sorting workspace projects now runs in linear time, fixing installs and lockfile updates that stalled for seconds on workspaces with thousands of projects forming deep dependency chains [#​14149](https://github.com/pnpm/pnpm/issues/14149), [#​14151](https://github.com/pnpm/pnpm/issues/14151).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://notion.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/notion.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/notion_light.svg" />
<img src="https://pnpm.io/img/users/notion.svg" width="80" alt="Notion" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://latitude.so/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/latitude.svg" width="160" alt="Latitude"></a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.24.0`](https://github.com/pnpm/pnpm/releases/tag/v11.24.0): pnpm 11.24
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.23.0...v11.24.0)
##### Minor Changes
- Added global build approvals [pnpm/pnpm#14101](https://github.com/pnpm/pnpm/issues/14101).
##### Patch Changes
- Fixed pnpm v11 incorrectly reporting `confirmModulesPurge` as unrecognized when set in `pnpm-workspace.yaml`. The Rust CLI now identifies the unsupported option as a pnpm v11 setting instead of suggesting an unrelated setting.
- `pnpm install --frozen-lockfile` no longer fails with `ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE` when the pinned pnpm version recorded in `pnpm-lock.yaml` has to be re-resolved before it can be installed. It runs the pnpm version the lockfile pins and leaves the lockfile unchanged [#​14124](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13966). A dependency that no project declares anymore is no longer reinstated by the merge, and the packages it was the only path to are dropped with it.
- Batch workspace publishing accepts a shared scope-specific credential, rejects mismatched credentials for a registry before publishing, and runs the `publish` and `postpublish` scripts after each completed registry group [pnpm/pnpm#14101](https://github.com/pnpm/pnpm/issues/14101).
- The Rust CLI now honors five settings it recognized but ignored: `updateNotifier`, `legacyDirFiltering`, `initAuthorName` / `initAuthorEmail` / `initAuthorUrl`, `initLicense`, and `initVersion`. `pnpm install` and `pnpm add` check once a day for a newer pnpm and print how to get it (turn it off with `updateNotifier: false`); a `{<dir>}` filter selector can go back to matching the subtree below the directory with `legacyDirFiltering: true`; and `pnpm init` writes the configured author, license, and version into the `package.json` it scaffolds. `PNPM_CONFIG_INIT_VERSION` is now read as well.
`maxsockets`, npm's spelling of `maxSockets`, is no longer ignored: both spellings are read from `pnpm-workspace.yaml`, the global config file, the environment, and the command line, in that increasing order of precedence — a value passed on the command line now wins even when the two sides spelled the setting differently.
A `lastUpdateCheck` timestamp dated in the future — after a clock change, a restored snapshot, or a hand-edited state file — no longer silences the update check until that time comes around.
`legacyDirFiltering` no longer reaches the workspace-root selectors pnpm generates for itself: the `!{<workspace-root>}` exclusion a recursive `run` / `exec` / `add` / `test` appends, and the `{<workspace-root>}` inclusion `--workspace-root` appends. Read as subtree matches they named every project below the root, so a recursive command under the setting selected nothing at all, and `--workspace-root` pulled in every project below the root instead of the root alone [#​14101](https://github.com/pnpm/pnpm/issues/14101).
- `pnpm install --frozen-lockfile` no longer fails when `pnpm-lock.yaml` records the pinned pnpm version alongside an engine package the running pnpm does not install it from. An entry pinning another version is still refused, and a plain install rewrites the block [#​14124](https://github.com/pnpm/pnpm/issues/14124).
### [`v11.23.0`](https://github.com/pnpm/pnpm/releases/tag/v11.23.0): pnpm 11.23
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.22.0...v11.23.0)
#### Minor Changes
- `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.
- The `registry` and `@scope:registry` entries show the merged routes rather than raw `.npmrc` values, so they always agree with the `registries` view.
- Settings that no supported pnpm version recognizes get their own warning. A key in the global config file that this version of pnpm does not read is no longer reported with advice to move it to a project-level `pnpm-workspace.yaml` (where it would be ignored too); the warning now says the setting is not recognized by this version of pnpm, names the pnpm version that does read it when there is one (for example, `globalShims` is a pnpm v12 setting), and suggests the closest real setting name when the key looks like a typo. Unrecognized and non-camelCase keys in a project's `pnpm-workspace.yaml`, previously ignored silently, are now reported the same way. `pnpm config get <key>` and `pnpm get <key>` no longer print config-load warnings, so a script capturing the value gets the value alone.
- The `importPackage` pnpmfile hook is deprecated. pnpm now prints a warning when a pnpmfile defines it, and the hook will be removed in the next major version. It also opts the installation out of the parallel package importer, making installation slower. If you rely on this hook, comment on [#​14101](https://github.com/pnpm/pnpm/issues/14101).
- `node_modules/.modules.yaml` no longer records the registries an install resolved from, and the recorded copy is dropped from the file on the first install that rewrites it.
It dated from the lockfile format that spelled a dependency's path relative to its registry, where reading an installed tree meant knowing the registries it was installed with. Dependency paths have not carried a registry for several major versions, and the recorded copy outlived its use: `pnpm list`, `pnpm why`, and single-project installs preferred it over the project's own configuration, so a project whose registry had changed since its last install was still read through the old one.
They now use the configured registries, like every other command already did.
- When `enableGlobalVirtualStore` is on, every process pnpm spawns for the project (`pnpm run`, `pnpm exec`, lifecycle scripts) now receives a `NODE_PATH` pointing at the project's hoisted `node_modules`, plus a `NODE_OPTIONS` `--import` flag that registers a resolve hook restoring `NODE_PATH` lookups for ESM imports. Dependencies that import undeclared ("phantom") packages keep resolving under the global virtual store — for both CommonJS and ESM — without installing the `@pnpm/plugin-esm-node-path` config dependency [pnpm/pnpm#9618](https://github.com/pnpm/pnpm/issues/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:
```yaml
resolutionMode: time-based
registries:
https://npm.internal.example/:
supportsTimeField: true
```
`registry.npmjs.org` omits `time` from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: `registrySupportsTimeField` answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a `time` field npmjs does not serve. The answer is now per registry, and `registrySupportsTimeField` remains the answer for every registry that does not declare one.
The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf.
- A pnpr resolve request now carries the client's registries the way the `registries` setting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one's `serverType` — in place of the prefix map it used to send.
The server routes them through the same inversion the config reader runs, so a pnpr-served install resolves a scoped dependency from the registry that scope is routed to, which it previously could not: only the default registry and the prefix-addressed ones reached the server. A declared `serverType` reaches it too, so the tarball URLs pnpr omits from the lockfile match the ones the client reconstructs.
Built-in scope routes the project has not pointed elsewhere are not declared, so a pnpr server's allowlist is not asked about `npm.jsr.io` on requests that resolve no JSR package.
A registry a request only declares is no longer refused up front for being off the server's allowlist — a client describes its whole configuration, including scopes a given resolve never reaches, so a stray `@scope:registry` in a developer's `~/.npmrc` no longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message.
This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned.
- The `registries` setting now declares a registry once, keyed by its URL, with everything about that registry in the entry: how it lays out tarball URLs, the scopes routed to it, and the bare-specifier prefix it answers to.
```yaml
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`:
- **undeclared** (the default) — strict. Only the exact canonical URL is treated as reconstructible.
- **`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](https://github.com/pnpm/get-npm-tarball-url/issues/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.
##### Migrating
The older `registries` shape, a map of `<scope>: <url>` strings, still works and needs no change:
```yaml
registries:
'@acme': https://npm.acme.example/
```
`namedRegistries` is deprecated in favor of the `prefix` field, and is still read for prefixes `registries` does not declare.
`toLockfileResolution` and `isCanonicalRegistryTarballUrl` now take their registry and layout as an options object rather than positional arguments, so `@pnpm/lockfile.utils` and `@pnpm/resolving.tarball-url` get a major bump.
- An install that had to re-hash store files to verify them now reports it. If that cost more than a second, it says how long — `The integrity of N files was checked in 2.5s.` — and if it was quick but covered more than a thousand files, it names the cause instead: their timestamps changed since the store recorded them, which a backup tool, an antivirus scan or a copied store can do.
- Added `virtualStoreType`, which names where the virtual store lives — one store per machine, or one per project:
```yaml
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.
#### Patch Changes
- `pnpm add --allow-build` now adds to the `allowBuilds` entries already in `pnpm-workspace.yaml` instead of replacing them [#​13872](https://github.com/pnpm/pnpm/issues/13872).
- Kept pending build approvals available after removing an unrelated dependency.
- `pnpm approve-builds` now removes `onlyBuiltDependencies`, `onlyBuiltDependenciesFile`, `neverBuiltDependencies`, and `ignoredBuiltDependencies` from `pnpm-workspace.yaml` when it writes `allowBuilds`. Those settings were replaced by `allowBuilds` in pnpm 11 and silently ignored since, so a workspace migrated from pnpm 10 kept them around looking active.
- `pnpm audit` no longer reports a patched version that was never published or is deprecated. The inferred patched range (e.g. `>=4.17.24` from `<=4.17.23`) is now checked against the registry packument, and the report is corrected to the lowest non-deprecated published version that satisfies it (e.g. `>=4.18.1` when `4.17.24` does not exist and `4.18.0` is deprecated). When no published version satisfies the range, the report shows `Patched versions: None`. This also prevents `pnpm audit --fix` from adding overrides or `minimumReleaseAgeExclude` entries for patches that do not exist [#​13824](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/11563).
The `--json` output of `pnpm audit` now returns `patched_versions: null` for advisories whose inferred patch is not available (never published, skipped, yanked, or deprecated), making it easier for tooling to distinguish "no fix available" from "fix available at version X".
- Fixed `pnpm patch-commit` in project and edit paths containing non-ASCII characters.
- The package and bump pickers of `pnpm change` now size their page from the terminal height instead of always showing 7 rows. They fall back to 7 rows when the terminal height is unknown [`pnpm/pnpm#13815`](https://github.com/pnpm/pnpm/issues/13815).
- Canceling a `pnpm change` prompt with Ctrl-c no longer prints a stack trace. It reports `Change canceled` and exits with a success status, like the other interactive commands [#​13814](https://github.com/pnpm/pnpm/issues/13814).
- Re-fetch full registry metadata when `minimumReleaseAge` is enabled and an abbreviated packument's `time` map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version [pnpm/pnpm#13741](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13765).
- Fixed `PNPM_CONFIG_NODE_VERSION` being ignored when setting the Node.js version used for compatibility checks.
- A custom fetcher can no longer replace the archive integrity that `pnpm-lock.yaml` pins: the locked value is restored after a `canFetch` or `fetch` hook rewrites the resolution, and delegating a locked archive to a directory or git source now fails instead of installing unverified content.
The Rust CLI now also loads the pnpmfiles named by the `pnpmfile` setting (a single path or an ordered list), and hands custom fetchers native `localTarball` and `remoteTarball` callbacks — including on a fresh install that has to compute a missing tarball integrity, which is then reused by later offline installs. File maps a fetcher returns are accepted only when they match what those native callbacks extracted.
- Fixed an issue where running `pnpm dedupe --check` in projects with `nodeLinker: hoisted` would cause dependencies to be moved out of `node_modules` into `node_modules/.ignored`.
- `pnpm deploy --prod` and `pnpm deploy --no-optional` no longer list the excluded dependency groups in the deployed `package.json` and `pnpm-lock.yaml`. The deployed lockfile referenced packages that the deploy left out of its graph, so installing in the deploy directory afterwards created dangling symlinks [#​13623](https://github.com/pnpm/pnpm/issues/13623).
- Don't treat files like `license16.json` as a package license when deciding if the workspace LICENSE file should be included in the packed package.
- `pnpm exec --recursive --no-reporter-hide-prefix` no longer prints a blank prefixed line after each chunk of a command's output, and no longer splits a line in two when it straddles a chunk boundary.
- Fixed `404` errors when installing from a registry that serves scoped packages only from a percent-encoded path, such as GitHub Enterprise Server. Outside `registry.npmjs.org`, a tarball URL that encodes the scope separator as `%2f` or `%2F` is no longer mistaken for one that pnpm can rebuild from the package name, version, and registry, so it is kept in `pnpm-lock.yaml` and requested verbatim on the next install [#​13534](https://github.com/pnpm/pnpm/issues/13534).
- Fixed `trustPolicyExclude` and `minimumReleaseAgeExclude` being ignored when set to a single string instead of a list. The value was read one character at a time, so the exclusion never matched the package it named — and a `*` anywhere in it matched every package, silently switching the policy off.
- `pnpm init` now pins the exact pnpm version instead of a `^` range, and records it in the `packageManager` field alongside `devEngines.packageManager`. Corepack reads only `packageManager` and accepts nothing but an exact version, so it rejected the generated `package.json` with "expected a semver version" [pnpm/pnpm#13969](https://github.com/pnpm/pnpm/issues/13969). A package created inside an existing workspace is still left unpinned — it follows the pin at the workspace root — and `--no-init-package-manager` still scaffolds a manifest without any pin. In pnpm 12, `pnpm init` also honors `initType` and its `--init-type` flag, so the manifest it writes is the same one pnpm 11 writes.
- Fixed an issue where package overrides were written into the metadata cache, causing removed overrides to keep applying on subsequent installs [pnpm/pnpm#13918](https://github.com/pnpm/pnpm/issues/13918).
- On Windows, upgrading pnpm no longer leaves a stale `pnpm.ps1` behind. PowerShell resolves `pnpm.ps1` ahead of `pnpm.cmd`, so a shim written by an older installation kept running the previous version. Linking the pnpm CLI's bins now deletes it [#​13919](https://github.com/pnpm/pnpm/issues/13919).
- Fixed an inconsistency where `minimumReleaseAgeExclude` (and `trustPolicyExclude`) wildcard/bare-name rules behaved differently in the evaluator and normalizer. A bare rule now consistently evaluates as matching every version, preventing unexpected behavior and silent widening of version policy exemptions when pnpm rewrites the workspace manifest [pnpm/pnpm#13725](https://github.com/pnpm/pnpm/issues/13725).
- A frozen install no longer rewrites the `packageManagerDependencies` block of `pnpm-lock.yaml`. When the pnpm version pinned by `devEngines.packageManager` (or by `packageManager`) is missing from the lockfile or no longer matches it, `--frozen-lockfile` now fails with `ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE` instead of resolving the version and saving it, so a manifest whose pin was bumped without regenerating the lockfile can no longer pass CI [#​14009](https://github.com/pnpm/pnpm/issues/14009).
- A git dependency installed over HTTPS from a hosted repository now keeps its branch, tag, or version range in the specifier recorded in `package.json`. It was written back without one, so the next `pnpm update` moved the dependency to the repository's default branch [#​13999](https://github.com/pnpm/pnpm/issues/13999).
- Fixed `pnpm update --global --latest` failing with a 404 error when a globally installed package was not added from the registry by name. Packages installed from a local path (`link:`/`file:`), a git repository, a tarball URL, an `npm:` alias, or a named registry now keep their spec during a global update instead of being looked up by name in the default registry. See [#​12854](https://github.com/pnpm/pnpm/issues/12854).
- Fix recursive `pnpm update <name>@<version>` so an exact pinned update stays scoped to the requested version line: copies of the same package on another major line — or, for a `0.x` request, another minor line — keep their locked resolution instead of being re-resolved along with the target.
- Under `nodeLinker: hoisted`, a dependency declared against a peer-resolution variant of a package version is no longer dropped from the installed layout. All variants of a version share one hoisted copy, and edges pointing at any of them now resolve to it, so the depending project keeps the package in its `.package-map.json` and the depending package keeps it in its `node_modules/.bin`.
- Fixed `pnpm install --merge-git-branch-lockfiles` deleting the per-branch lockfiles when the `lockfile` setting is `false`. Such an install never reads them, so it has nothing to merge them into and now leaves them alone.
- Fixed `pnpm install` sometimes not exiting after printing `Done in Xs` [#​12297](https://github.com/pnpm/pnpm/issues/12297).
- Fixed pnpm failing to read `.modules.yaml` files containing long dependency paths [#​13875](https://github.com/pnpm/pnpm/issues/13875). The manifest is now parsed as JSON (the format pnpm writes it in), falling back to the YAML parser only for manifests written by old pnpm versions.
- With `preferSymlinkedExecutables`, `NODE_PATH` again points at the virtual store of the workspace root when pnpm is run from inside a workspace package, so scripts can resolve dependencies that live only in the hoisted store [#​13912](https://github.com/pnpm/pnpm/issues/13912).
- Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win [pnpm/pnpm#13976](https://github.com/pnpm/pnpm/issues/13976).
- `pnpm pkg get` and `pnpm pkg set` now accept hyphens inside a dot-notation property path, so `pnpm pkg get dependencies.some-package-name` reads the key instead of failing with `ERR_PNPM_UNEXPECTED_TOKEN_IN_PROPERTY_PATH`. The bracketed and quoted forms already worked and are unchanged.
- A resolve request now carries the client's `resolutionMode`, so an install delegated to a pnpr server picks versions the way the client would. `time-based` and `lowest-direct` reached the server as nothing at all, leaving it on its `highest` default: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored.
This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving `highest`; the protocol is still experimental and unversioned.
- Fixed `pnpm` installs using pnpr to honor the client's `autoInstallPeers`, `dedupePeers`, and `excludeLinksFromLockfile` settings [pnpm/pnpm#13389](https://github.com/pnpm/pnpm/issues/13389).
- `pnpm remove` now prunes undecided entries (`"set this to true or false"`) from `allowBuilds` in `pnpm-workspace.yaml` when `sharedWorkspaceLockfile: true` and the corresponding packages are removed [pnpm/pnpm#13892](https://github.com/pnpm/pnpm/issues/13892).
- Fixed workspace discovery for `pnpm-workspace.yaml` files without a `packages` field so commands only consider the workspace root instead of recursively scanning nested projects [#​14047](https://github.com/pnpm/pnpm/issues/14047).
- A runtime installed through `devEngines.runtime` now matches the host when `supportedArchitectures` lists several platforms. Listing `os: [darwin, linux]` and `cpu: [x64, arm64]` used to install the runtime built for the first entry of each list, so a machine running Linux on arm64 got a macOS x64 Node.js that could not execute [#​13898](https://github.com/pnpm/pnpm/issues/13898).
- `pnpm sbom` now fails with `ERR_PNPM_SBOM_MISSING_IMPORTERS` when `pnpm-lock.yaml` has no entry for a selected project, instead of writing an SBOM that under-reports that project's dependencies. Previously this crashed with `Cannot read properties of undefined (reading 'devDependencies')`.
- `pnpm self-update` now rewrites a simple `devEngines.packageManager.version` range (`^`/`~`) to the newly installed version, keeping the operator — matching how `pnpm update` and `pnpm runtime set` rewrite ranges. Complex ranges such as `>=8.0.0` that the new version satisfies are still left unchanged [#​13935](https://github.com/pnpm/pnpm/issues/13935).
- `pnpm self-update <tag>` no longer downgrades when the dist-tag points at the pnpm version already running and that version is younger than `minimumReleaseAge`. The maturity cutoff moved the tag back to the previous mature release, so `pnpm self-update next-12` on v12.0.0-rc.4 switched to v12.0.0-rc.3.
- `pnpm set-script` now updates `package.json` instead of failing with `ERR_PNPM_NOT_IMPLEMENTED` [`pnpm/pnpm#13956`](https://github.com/pnpm/pnpm/issues/13956).
- `pnpm update` now preserves the existing range operator when updating a prerelease dependency. See [#​7002](https://github.com/pnpm/pnpm/issues/7002).
- Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges (`"*"`, `"^1.2.3"`) rather than the `workspace:` protocol. With `preferWorkspacePackages` enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install.
- Added `fetchWarnTimeoutMs` and `fetchMinSpeedKiBps` to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters [pnpm/pnpm#12042](https://github.com/pnpm/pnpm/issues/12042).
- An override change is now absorbed by the fast lockfile update even when another, unchanged override uses the `catalog:` protocol. Previously any `catalog:`-valued override forced a full re-resolution whenever the override list changed, which could move unrelated packages in the lockfile (for example after `pnpm audit --fix` added an override).
- Packed workspace package manifests now preserve dependency order, making repeated `pnpm pack` output deterministic [#​10167](https://github.com/pnpm/pnpm/issues/10167).
- `pnpm update <name>@<version>` now fails with `ERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEP` when the package is not a direct dependency of any selected project, instead of quietly updating it to whatever a fresh install would resolve. There is nowhere to record the version in that case, so the request cannot be honored, and the error points at the `overrides` entry that does pin a transitive dependency. Ranges and tags are unaffected, and a package that any selected project declares directly still takes its version as before.
- `trustPolicy: no-downgrade` no longer aborts the install with `ERR_PNPM_MISSING_TIME` on registries that serve no per-version `time` field when `minimumReleaseAgeIgnoreMissingTime` is set. The trust check reads the same publish dates the `minimumReleaseAge` check does, so it now honors the same opt-in and skips the affected package with a warning [#​12446](https://github.com/pnpm/pnpm/issues/12446).
`minimumReleaseAgeIgnoreMissingTime` no longer lets a lockfile entry the registry does not list pass the `minimumReleaseAge` check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure.
The missing-`time` warning now names the check it is reporting on, so a package whose `minimumReleaseAge` and `trustPolicy` checks are both skipped warns about both instead of only the first.
- `pnpm update <pkg>@<version>` now updates only the selected packages and leaves unrelated dependencies unchanged. A selector that renames the package it installs — `pnpm update <alias>@npm:<pkg>@<version>` or the `jsr:` equivalent — now targets the package the alias installs rather than the alias.
- Fixed `verifyDepsBeforeRun` being ignored when set to `install`, `warn`, `error`, or `prompt` through the `PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN` environment variable or the `--config.verify-deps-before-run` flag [#​13816](https://github.com/pnpm/pnpm/issues/13816). Only the boolean values were accepted before, so a string value was silently dropped.
- `pnpm version <bump>` with `--dry-run` no longer edits `package.json` files. It now only reports the bumps it would make, and skips the working tree check, the version lifecycle scripts, the commit, and the tag [`pnpm/pnpm#13953`](https://github.com/pnpm/pnpm/issues/13953).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.22.0`](https://github.com/pnpm/pnpm/releases/tag/v11.22.0): pnpm 11.22
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.21.0...v11.22.0)
#### Minor Changes
- Added `pnpm cache path`, which prints the directory pnpm uses for its metadata cache. CI setups can use it to cache that directory — including the lockfile verification log, which lets a job skip re-checking an unchanged lockfile against the configured supply-chain policies.
- `--config.config-dir` no longer reaches the config through a project's `pnpm-workspace.yaml`, and neither do the `--config.` spellings of the other settings a project manifest may no longer contribute (`--config.pnpm-home-dir`, `--config.workspace-dir`, `--config.global-pkg-dir`, `--config.root-project-manifest-dir`). None of them was ever a supported way to set those directories: pnpm resolves them from the environment, and these flags took effect only because the project-manifest merge re-applied the command line afterwards. The dedicated flags, such as `--dir` and `--global-dir`, are unaffected [#​13629](https://github.com/pnpm/pnpm/issues/13629).
- `pnpm config set` refuses to write a setting to a project's `pnpm-workspace.yaml` that pnpm does not read from there, rather than leaving a key in the file that does nothing. Those settings are `configDir`, `pnpmHomeDir`, `stateDir` and the others that name machine-level state. The command fails with `ERR_PNPM_CONFIG_SET_NOT_A_PROJECT_SETTING`, naming where the setting does belong when it belongs somewhere. `pnpm config delete` still clears one that a file already carries, in whichever spelling it uses [#​13629](https://github.com/pnpm/pnpm/issues/13629).
- Added a new setting `minimumReleaseAgeExcludePrune`. When enabled, `pnpm add`, `pnpm update`, and `pnpm remove` prune the entries of `minimumReleaseAgeExclude` in `pnpm-workspace.yaml` that the freshly written lockfile no longer resolves: versions that are gone are dropped (an entry is removed once none of its versions remain), and entries for packages that are no longer in the lockfile are removed too. Name patterns (`@scope/*`) are always kept. The cleanup is skipped when the install's lockfile does not cover the whole workspace (`sharedWorkspaceLockfile: false`), since entries another project still needs would look stale.
Renamed `cleanupUnusedCatalogs` to `catalogPrune`, so that catalog pruning and release-age exclude pruning use one vocabulary. `cleanupUnusedCatalogs` continues to work; when both are set, `catalogPrune` wins.
- A project's `pnpm-workspace.yaml` can no longer choose where pnpm keeps its credentials, its own installation, or the registry it downloads its next version from. One of those settings is `configDir`, which decided where `pnpm login` writes the granted token. `bin`, `dir`, `globalBinDir`, `globalDir`, `npmrcAuthFile`, `pnpmHomeDir`, `stateDir`, `userconfig` and `workspaceDir` are ignored there now too, and pnpm warns about the ones it finds. `cacheDir` and `storeDir` are unaffected [#​13629](https://github.com/pnpm/pnpm/issues/13629).
- Resolving a Node.js runtime version (`devEngines.runtime` / `runtime:` specifiers) is now much faster: the per-version release metadata is cached in the pnpm cache directory after its signature is verified, and an exact stable version such as `runtime:22.23.2` no longer downloads the Node.js release index. A pinned runtime whose metadata was fetched once resolves without any network access, which removes the noticeable delay on the first `node` invocation in a project pinning an already-downloaded runtime [#​13899](https://github.com/pnpm/pnpm/issues/13899).
#### Patch Changes
- Fixed intermittent `ERR_PNPM_ENOENT` and `ERR_PNPM_ENOTEMPTY` errors while renaming `_tmp_*` directories during installation with `nodeLinker: hoisted`, in workspaces that also use `patchedDependencies`.
- `pnpm add` no longer re-resolves the dependency graph when `pnpm-lock.yaml` already holds a version satisfying the request — promoting a transitive dependency to a direct one, or adding to a second workspace package what a first one already depends on, now only saves the dependency in `package.json` and records its importer entry. A satisfying locked version is necessary but not sufficient: the install still falls back to a full resolution for a dist tag, an alias, a `workspace:`/`catalog:`/git/tarball specifier, `--save-peer`, an overridden package, a `catalogMode` other than `manual`, and — under `resolutionMode: time-based` or `lowest-direct`, which resolve a direct dependency to the low end of its range — a range several locked versions satisfy.
- Global installs now switch over atomically. The command shims in the global bin directory point at a stable per-package link rather than at the directory a particular install produced, so `pnpm add -g` and `pnpm update -g` activate a new version by moving that one link instead of rewriting every shim. A command can no longer be missing from `PATH` while an install is in progress, and a failed install leaves the previous version in place.
- `pnpm audit --fix` and `pnpm audit --fix update` no longer add `minimumReleaseAgeExclude` entries for patched versions that were published before the `minimumReleaseAge` cutoff. The publish time of each minimum patched version is now checked against the registry metadata, and only versions young enough to be blocked by the age gate get an exclusion entry [#​11563](https://github.com/pnpm/pnpm/issues/11563).
- `pnpm add <pkg>@<version>` and `pnpm update <pkg>@<version>` under a non-manual `catalogMode` now move the catalog entry's resolution to the requested version. Previously, when the catalog entry was a range that covered the requested version but resolved to a different one, the request was dropped silently: nothing was installed, nothing was written, and no error was raised.
- A project that wasn't part of an install that moved a catalog entry now follows the entry the next time it is installed. It used to keep the version the entry resolved to before — a version the entry no longer allowed — and no later install corrected it, so one catalog entry ended up resolved to two versions.
- `pnpm add <pkg>@<version>` and `pnpm update <pkg>@<version>` under `catalogMode: strict` no longer fail with `ERR_PNPM_CATALOG_VERSION_MISMATCH` when the catalog entry is a range that the wanted version satisfies. The dependency keeps using the catalog; only a version that really falls outside the catalog's range is rejected [#​13715](https://github.com/pnpm/pnpm/issues/13715).
- A changed `catalogs` or `pnpm.overrides` block no longer has to be the only change for `pnpm install` to update the lockfile in place. Editing an override while also removing a dependency, or changing a catalog entry in the same commit as a range bump, is now absorbed in one pass instead of re-resolving the whole dependency graph [#​13799](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13763).
- `pnpm deploy` injects workspace dependencies again, so the deploy directory is self-contained instead of symlinking back into the source workspace [#​13754](https://github.com/pnpm/pnpm/issues/13754). Enabling `injectWorkspacePackages` with `dedupeInjectedDeps` disabled now also rewrites already-linked workspace dependencies to injected copies.
- `pnpm deploy --no-optional` no longer writes a lockfile whose snapshots reference optional dependencies that the deploy excluded.
- Removing the last dependency that references a catalog entry via the fast lockfile update no longer leaves the stale catalog entry in `pnpm-lock.yaml`.
- A git dependency whose clone (or shallow fetch) fails now reports which package it belongs to, under the `ERR_PNPM_GIT_FETCH_FAILED` code, with credentials in the repository URL redacted. When the lockfile records an SSH remote, the error also explains that fetching it needs an SSH key for that host, and that a lockfile entry written before pnpm v11.21 can be re-recorded over HTTPS with `pnpm update <package>` [#​13743](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13696).
- `syncInjectedDepsAfterScripts` no longer fails with `ERR_PNPM_UNSUPPORTED_INODE_TYPE` when a workspace package contains an inode that is neither a file nor a directory, such as the FIFO 1Password's environments create for `.env`. Such an inode cannot be hardlinked into the injected copy, so it is skipped and the rest of the package still syncs [#​13550](https://github.com/pnpm/pnpm/issues/13550).
`syncInjectedDepsAfterScripts` also no longer fails with `EEXIST` when a workspace package replaced a file with a directory of the same name since the injected copy was last synced.
- `syncInjectedDepsAfterScripts` no longer fails with `ENOTDIR` when a workspace package replaced a directory with a file of the same name and the injected copy still held that directory's contents.
- `syncInjectedDepsAfterScripts` now removes the bin link of a bin the script dropped. Previously only new bins were linked, so a build step that stopped declaring one left its shim behind, pointing at a command that was no longer there.
- `syncInjectedDepsAfterScripts` now identifies a file by its device as well as its inode number. An inode number is only unique within one filesystem, so on its own it could match an unrelated file on another device and leave that path stale in the injected copy.
- `pnpm store prune` no longer deletes the lockfile verification log. The log records which lockfile passed which supply-chain policies, so it stays valid across a prune of the store; keeping it lets the next install skip re-verifying an unchanged lockfile.
- Widening a dependency's range no longer leaves the project on an older version. The lockfile update now points the project at the highest version of that dependency already in the lockfile that satisfies the new range — matching what a full resolution records — instead of keeping the locked version whenever it happened to satisfy, which could leave a duplicate behind. A range change that only an already-locked version satisfies is now also handled without re-resolving [#​13778](https://github.com/pnpm/pnpm/issues/13778).
- `resolutionMode` is no longer ignored when `minimumReleaseAge` is in effect. `lowest-direct` and `time-based` pick the lowest satisfying version of a direct dependency again; previously any active release-age cutoff — including the built-in default — silently forced the highest, so `resolutionMode` only worked when `minimumReleaseAge: 0` was set explicitly [#​13752](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13696).
- `pnpm config delete <key>` no longer fails with `ENOENT` when the config file it would edit does not exist. Clearing a setting that was never set is a no-op [#​13651](https://github.com/pnpm/pnpm/issues/13651).
- Changing a `pnpm.overrides` entry to a version range now updates the lockfile in place when a version the lockfile already holds satisfies the range, instead of re-resolving the whole dependency graph. Only exact versions were handled before [#​13696](https://github.com/pnpm/pnpm/issues/13696).
- Changing a parent-scoped `pnpm.overrides` entry (`"parent>child": "2.0.0"`) now updates the lockfile in place instead of re-resolving the whole dependency graph. Only the named parent's dependency moves; every other package keeps the version it had [#​13795](https://github.com/pnpm/pnpm/issues/13795).
- Removing a dependency, or moving one to another already-locked version, no longer re-resolves the whole dependency graph just because some package resolves a peer with the same name. The lockfile update now compares the peer suffixes against the exact `name@version` the removal severed, so a suffix that names a different — still present — version of that dependency is left alone [#​13781](https://github.com/pnpm/pnpm/issues/13781).
- Projects with a pnpmfile now use the fast lockfile update paths: an unchanged pnpmfile (proven by the recorded `pnpmfileChecksum`) no longer forces a full re-resolution for removals, dependency group moves, compatible range changes, and the other in-place lockfile rewrites [#​13696](https://github.com/pnpm/pnpm/issues/13696).
- A lockfile entry whose resolution is unchanged no longer loses its recorded `deprecated` marker when a registry serves the package's metadata inconsistently — re-resolving to the same version keeps the deprecation instead of silently dropping the line [#​13846](https://github.com/pnpm/pnpm/issues/13846).
- `pnpm prune` is now recursive by default inside a workspace, just like `pnpm install`. This fixes `pnpm prune --prod` in a workspace root emptying the `node_modules` directories of the other workspace projects, dropping the links to the workspace packages they depend on in production [#​13718](https://github.com/pnpm/pnpm/issues/13718).
- A setting written in kebab-case in the global `config.yaml` is now reported instead of being silently ignored [#​13650](https://github.com/pnpm/pnpm/issues/13650).
- `pnpm remove` no longer re-resolves the dependency graph. The removed dependency's entries are dropped from `pnpm-lock.yaml` and anything they made unreachable is pruned, without registry access. The install still falls back to a full resolution when a surviving package resolves a peer dependency through the removed one.
- Removing a package from a workspace no longer forces a full re-resolution. The lockfile update drops the departed project's importer entry and prunes whatever only it depended on. A project that is still linked from a surviving project continues to be reported as an error [#​13696](https://github.com/pnpm/pnpm/issues/13696).
- An install sharing a global virtual store no longer removes an incomplete package directory that another importer is still writing, which could fail with `failed to remove existing directory ... prior to swap: Directory not empty`. Such a directory is now repaired in place, and a package file left damaged by an interrupted install is restored instead of being kept.
- `pnpm sbom` no longer emits components for optional platform-specific dependencies that cannot be installed on the current platform (for example, the native `@rolldown/binding-*` variants for other operating systems). Such packages are present in the lockfile but are never downloaded, so their license (and other metadata) could not be resolved and they appeared in the SBOM without one. `pnpm sbom --lockfile-only` still describes the whole lockfile graph, which is platform-independent by design.
- An `ssh://` git dependency pointing at a bracketed IPv6 host, such as `ssh://[::1]/repo.git`, is resolved now. Its colons were read as an SCP-style path separator, which turned the address into `[:/1]` and left the specifier unresolvable. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, an `ssh://` git dependency written without user info — `ssh://git.example.com/team/repo.git`, `git+ssh://git.example.com:2222/team/repo.git` — no longer fails with `TypeError: Cannot read properties of undefined (reading 'includes')`. Only the `user@host` form worked before.
- `packageExtensions` is now validated when the configuration is read, so a malformed entry (for instance a dependency range set to `null`) fails with an actionable error instead of crashing later during peer dependency resolution [#​13756](https://github.com/pnpm/pnpm/issues/13756).
- Projects using `resolutionMode: time-based` now benefit from the fast lockfile update paths. A removal, a dependency group move, or a compatible range change no longer forces a full re-resolution just because the lockfile carries a `time` field [#​13696](https://github.com/pnpm/pnpm/issues/13696).
- An install that drops the last dependent of a patched package no longer updates the lockfile in place and succeeds silently. Removing a dependency, widening `ignoredOptionalDependencies`, or adding a removal override could each prune the package while the patch stayed configured; such an install now falls back to a full resolution, which reports the unused patch with `ERR_PNPM_UNUSED_PATCH`. Under `allowUnusedPatches`, where the lockfile update is kept, the same install now warns that the patch went unused instead of saying nothing [#​13827](https://github.com/pnpm/pnpm/issues/13827).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.21.0`](https://github.com/pnpm/pnpm/releases/tag/v11.21.0): pnpm 11.21
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.20.0...v11.21.0)
#### Minor Changes
- Added interactive group selection to `pnpm update --global --interactive`.
- Running `pnpm setup`, `pnpm self-update`, or a command that modifies the global installation (such as `pnpm add --global`) through `sudo` now prints a warning. pnpm keeps global packages and configuration in the invoking user's home directory, so running these commands as root silently operates on the root user's home directory instead of yours. They will fail with `ERR_PNPM_SUDO_NOT_SUPPORTED` in pnpm v12. Read-only global commands (such as `pnpm bin --global`) are unaffected.
#### Patch Changes
- Fixed pnpm failing to start under asynchronous Node.js module loaders when no `.pnpmfile.mjs` exists [pnpm/pnpm#11701](https://github.com/pnpm/pnpm/issues/11701).
- Fixed `minimumReleaseAge` fallback for custom dist-tags so the selected version does not exceed the registry’s original tag target.
- Removing a dependency from `package.json` and reinstalling no longer re-resolves the dependency graph. The importer's entry is dropped from `pnpm-lock.yaml`, anything it made unreachable is pruned, and a catalog entry that loses its last referent is removed — all without registry access. Installs still fall back to a full resolution when a package that stays resolves a peer dependency through the removed one, since that would change the surviving package's entry rather than only prune.
- Changing a catalog entry to a different exact version no longer re-resolves the dependency graph. The package is replaced in `pnpm-lock.yaml` directly, reusing the same check the `pnpm.overrides` fast path applies: every locked dependency of the package must still satisfy the new version's manifest. Installs fall back to a full resolution when anything other than the catalog reaches the package — an importer that depends on it directly, or another package that depends on it — since the graph would then need both versions.
- Fixed a CI regression where `github:owner/repo` dependencies (and other shorthand Git specifiers) would fail to install with `Permission denied (publickey)` on CI runners that lack SSH keys. The Git resolver no longer records an SSH URL unless the user explicitly wrote one (e.g. `git+ssh://` or `git@host:...`):
- The repository visibility probe (an HTTP HEAD request) now retries transient failures such as `429 Too Many Requests`, so host throttling of CI runners is no longer mistaken for a private repository.
- For non-SSH specifiers, anonymous HTTPS `git ls-remote` access is now tried before SSH, so a public repository whose visibility probe fails still resolves to a portable HTTPS URL instead of an SSH URL that only works where SSH keys are configured.
- When every probe fails, the resolver falls back to HTTPS for shorthand and HTTPS-style specifiers, and only guesses SSH when the user explicitly provided an SSH URL.
- A repository that could not be confirmed public is no longer resolved to the host's anonymous archive URL (e.g. `codeload.github.com`, which would fail to download for a private repository); it stays a regular `git` resolution so installs can use ambient Git credentials such as credential helpers and tokens.
Note that a private repository that is reachable both over authenticated HTTPS and over SSH now resolves to its HTTPS URL, where previous versions recorded the SSH URL.
Fixes [pnpm/pnpm#13276](https://github.com/pnpm/pnpm/issues/13276).
<!-- cspell:ignore publickey -->
- `ng build` and `nuxt build` now work under the global virtual store: pnpm's built-in compatibility extensions add the `tslib` dependency that `@angular/build` uses without declaring and the `unplugin` dependency that `@nuxt/vite-builder` v4 uses without declaring.
- Fixed `link:` dependencies under `enableGlobalVirtualStore` so linked children are materialized and slots remain isolated by their resolved link targets.
- An install that skips resolution because `pnpm-lock.yaml` is already up to date now reacts fully to packages the lockfile removed — for example after pulling a lockfile in which a dependency was deleted. The hoist layer is recomputed, so a package that became hoistable when a direct dependency was removed is hoisted, and `pendingBuilds` entries for removed packages are dropped instead of staying pending forever.
- The held-back-update warning printed by `pnpm update` no longer fires when `minimumReleaseAge` is the actual reason a newer version was not picked. The warning's baseline now applies the same maturity cutoff as the pick itself, so it no longer wrongly attributes the hold-back to "your manifests and already installed dependencies" or recommends an override that would defeat the age gate. See [#​13071](https://github.com/pnpm/pnpm/issues/13071).
- Checking whether `ignoredOptionalDependencies` is up to date no longer reorders the configured patterns. The check sorted them in place, which could move an `!` exclusion ahead of the pattern it excludes from and flip which optional dependencies were ignored.
- Changing `autoInstallPeers`, `dedupePeers`, `peersSuffixMaxLength`, `excludeLinksFromLockfile`, or `injectWorkspacePackages` no longer re-resolves the dependency graph when the lockfile proves the setting cannot affect it: no package or project declares a peer dependency for the peer settings, and no project depends on a directory or on another workspace project for the link and injection settings. The new setting is recorded in `pnpm-lock.yaml` and the install proceeds from the existing resolution. Every other case still falls back to a full resolution.
- Adding, editing, or removing an entry in `patchedDependencies` no longer re-resolves the dependency graph. Resolution never reads a patch — it only records the patch file's hash against the package it matches — so the install now rewrites the affected entries in `pnpm-lock.yaml` and materializes the patched package from the store instead. Installs still fall back to a full resolution when the patched package is reachable as a peer dependency, and when the new configuration would leave a patch unused while `allowUnusedPatches` is off, so `ERR_PNPM_UNUSED_PATCH` is still reported.
- Resolving a private git repository no longer blocks on an interactive credential prompt: `git ls-remote` now fails fast with an authentication error when git has no credentials for the repository [#​13522](https://github.com/pnpm/pnpm/issues/13522).
- Lockfile verification now honors offline mode by using cached registry metadata instead of reaching the registry. When the required metadata is not available locally, verification reports the same `ERR_PNPM_NO_OFFLINE_META` condition used by offline resolution.
- POSIX shell shims now follow symbolic links before computing `basedir`, preventing execution failures when a shim is invoked via an external symlink on `PATH` [#​13405](https://github.com/pnpm/pnpm/issues/13405).
- The automatic `packageManager` version switch works again on registries whose tarball URLs point at a different host than the registry itself (load-balanced feed proxies, Artifactory-style mirrors). Package-manager entries are now always recorded with integrity-only resolutions — the download URL is derived from the trusted bootstrap registry instead — and entries persisted in an invalid shape by an earlier pnpm are discarded and re-resolved instead of failing every command [#​13619](https://github.com/pnpm/pnpm/issues/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](https://github.com/pnpm/pnpm/issues/13147). When the configured registry cannot provide a verifiable signature, pnpm now fetches the signature from `registry.npmjs.org` and verifies it against the same embedded npm keys over the installed integrity — which proves exactly the same thing. If no signature can be obtained from either source (for example, both are unreachable, or the registry publishes only a `shasum`), pnpm proceeds with a warning instead of failing, but only when the packages resolve through a registry configured in the user's own (non-project) configuration; the download stays pinned by the lockfile integrity, and a signature that exists but does not validate still fails the switch.
- `pnpm fetch`, and any install run with `virtualStoreOnly`, no longer writes a `.pnp.cjs` loader under `nodeLinker: pnp`. These installs populate the virtual store without linking the project, so the loader would have claimed the project resolves out of a store it was never linked into. The importer links and `node_modules/.package-map.json` were already skipped; the PnP loader now follows the same rule.
- Prevent pnpm from removing project files when `modulesDir` resolves to the project root.
- Speed up installs after adding `ignoredOptionalDependencies` patterns by removing newly ignored optional dependencies and pruning packages that are no longer reachable without resolving the dependency graph again.
- When a failed install re-copies a bin script from the store, rerunning `pnpm install` now reapplies the executable bit to the bin instead of leaving it non-executable [#​12742](https://github.com/pnpm/pnpm/issues/12742).
- `pnpm root -g` and `pnpm bin -g` now print warnings to stderr instead of stdout, so their stdout stays a clean, machine-readable path. Previously, running either command with `--global` in a project that pins a package manager (e.g. via the `packageManager` field) printed a warning like `[WARN] Using --global skips the package manager check for this project` ahead of the path, breaking programs that capture the output as a path [#​13672](https://github.com/pnpm/pnpm/issues/13672).
In pnpm 12, `pnpm root -g` and `pnpm prefix -g` are now supported (they previously failed with `ERR_PNPM_CLI_ROOT_GLOBAL_UNSUPPORTED` / `ERR_PNPM_CLI_PREFIX_GLOBAL_UNSUPPORTED`), and the reporter output of `dlx`, `create`, `config`, `sbom`, `with`, `store`, `prefix`, `root`, and `bin` goes to stderr, matching pnpm 11.
- `pnpm setup` no longer makes Node.js print a `MODULE_TYPELESS_PACKAGE_JSON` warning about `dist/worker.js` on every command. The `package.json` it writes next to a standalone executable now declares `"type": "module"`.
- `pnpm update` without saving no longer records a version that the manifest's range excludes. The kept range stays authoritative: a requested version outside it is skipped with a warning, and a requested range, a dist tag, or `--latest` resolves within it instead of past it. Previously each of these could write a lockfile entry that contradicted its own specifier, which the next `pnpm install --frozen-lockfile` rejected with `ERR_PNPM_OUTDATED_LOCKFILE` [#​12764](https://github.com/pnpm/pnpm/issues/12764).
- `pnpm version -r --json` now outputs `[]` instead of human-readable text when no pending changes exist [`pnpm/pnpm#13217`](https://github.com/pnpm/pnpm/issues/13217).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.20.0`](https://github.com/pnpm/pnpm/releases/tag/v11.20.0): pnpm 11.20
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.19.0...v11.20.0)
#### Minor Changes
- **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.
##### If you use named registries
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`:
```yaml
namedRegistries:
work: https://npm.enterprise.example.com/
```
##### New built-in `npmjs:` alias
`npmjs:` 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:
```json
{ "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:**
```yaml
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.
#### Patch Changes
- 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](https://github.com/pnpm/pnpm/issues/13533).
`proxy=false` in the `.npmrc` or `proxy: false` in `pnpm-workspace.yaml` now turns proxying off instead of being read as a proxy host named `false`. `false` and `null` on `https-proxy` / `http-proxy` / `no-proxy` read as unset, and on the command line they are ordinary host names, since a flag carries its value verbatim.
- The env lockfile no longer pins `@pnpm/exe` alongside `pnpm` when the wanted pnpm version is 12 or newer. From v12 the unscoped `pnpm` package is itself the native executable, so `@pnpm/exe` is not published for it and resolving it would fail. The engine identity check now verifies the native binary through whichever package ships it.
- `lexCompare` and `nerfDart` are now published as `@pnpm/text.ordinal-comparator` and `@pnpm/config.registry-auth-key`. Use these instead of `@pnpm/util.lex-comparator` and `@pnpm/config.nerf-dart`.
- Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
- Dependency resolution is faster: package metadata is now filtered once per packument instead of once per dependency edge when `minimumReleaseAge` is active, and parsed semver versions and ranges are reused instead of re-parsed on every comparison.
- Security: `pnpm rebuild` now refuses a lockfile whose `packages` key carries a path traversal in the package name (e.g. `../../../escaped@1.0.0`), instead of running that package's lifecycle scripts and linking its bins in a directory outside the virtual store. Such a name is rejected with `ERR_PNPM_INVALID_DEPENDENCY_NAME`.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.19.0`](https://github.com/pnpm/pnpm/releases/tag/v11.19.0): pnpm 11.19
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.18.0...v11.19.0)
#### Minor Changes
- `pnpm login` no longer requires an interactive terminal when the registry supports web-based login: without a TTY it prints the authentication URL (skipping the QR code and the "Press ENTER to open the URL in your browser" prompt) and polls the registry until the browser approval completes. Only the classic username/password login still fails with `ERR_PNPM_LOGIN_NON_INTERACTIVE` in a non-interactive terminal.
- The `save-prefix` setting now accepts `=`: newly added dependencies are saved with an explicit `=` operator (`=1.2.3`) instead of the setting being silently treated as the default `^`.
#### Patch Changes
- `allowBuilds` entries can now approve git-hosted packages that pnpm downloads as a tarball, such as `github:` dependencies (which are fetched from `codeload.github.com` rather than cloned), by their repository URL without the resolved commit hash. This matches the hashless `git+` matching already supported for cloned git dependencies. For example:
```yaml
allowBuilds:
"foo@git+https://github.com/org/foo.git": true
```
This approves the package whether pnpm clones it or downloads a tarball, so the entry no longer has to be updated every time the pinned commit changes. GitLab and Bitbucket tarball downloads are matched the same way. Approving or denying a specific resolved commit by its full tarball dep path continues to work.
- `pnpm outdated --include-github-actions` no longer blocks on an interactive git credential prompt when a workflow uses a private action repo.
- Prevented `minimumReleaseAge` from replacing `latest` with a SemVer-greater version than the registry tag target [#​13034](https://github.com/pnpm/pnpm/issues/13034).
- Fixed empty `bundledDependencies` and `bundleDependencies` arrays causing nondeterministic lockfile changes. See [#​13123](https://github.com/pnpm/pnpm/issues/13123).
- The install summary no longer prints `(X is available)` when the registry's `dist-tags.latest` is still held back by the active `minimumReleaseAge` policy. The hint only ever names the actual latest tag, so an immature latest suppresses the hint instead of advertising the version pnpm just refused to install [#​11698](https://github.com/pnpm/pnpm/issues/11698).
- `pnpm update` keeps the explicit `=` operator of an exact version pin: a dependency saved as `=3.5.1` now updates to `=3.5.2` instead of the bare `3.5.2`. See [#​13168](https://github.com/pnpm/pnpm/issues/13168).
- Preserve a workspace dependency's `link:` entry when a run does not target it — e.g. `pnpm update <other-pkg>` (with or without `--recursive`), or a plain install after a root/catalog dependency change — with `injectWorkspacePackages`, instead of spuriously rewriting it to a peer-suffixed `file:` protocol. See [#​10433](https://github.com/pnpm/pnpm/issues/10433).
- Workspace dependencies declared with a relative path (e.g. `"foo": "workspace:../foo"`) are no longer silently dropped from the workspace projects graph, so `--filter` selection and the topological order of recursive commands take them into account.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.18.0`](https://github.com/pnpm/pnpm/releases/tag/v11.18.0): pnpm 11.18
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.17.0...v11.18.0)
#### Minor Changes
- Fixed an installed optional dependency being left without one of its own required dependencies. When a package reached through `optionalDependencies` is installable on the current system but one of its regular `dependencies` is not, a lockfile-based install skipped that dependency and installed the parent anyway, so importing the parent failed with `MODULE_NOT_FOUND`. The dependency is now installed, and an install-check warning reports the incompatibility. A dependency is still only skipped when every path to it is optional, or when the package that pulls it in was itself skipped [#​13286](https://github.com/pnpm/pnpm/issues/13286).
- `pnpm setup` now appends `PNPM_HOME` and the global bin directory to the GitHub Actions environment files (`GITHUB_ENV` and `GITHUB_PATH`), so later steps in the same job can run `pnpm add --global` and other global commands [#​9191](https://github.com/pnpm/pnpm/issues/9191).
- Added support for `publishConfig.name`, which publishes a package under a different name than the one its manifest carries in the workspace. It is for a project whose published name is already taken by a sibling project, which otherwise has to be renamed by a build step just before publishing. Only the published artifact is renamed — dependents, `pnpm-lock.yaml`, and release tooling keep addressing the project by its manifest name — and the new name reaches the packed manifest, the tarball filename, and everything that addresses the package at the registry: the already-published check of `pnpm publish -r`, its registry selection, and the release-planning probes of `pnpm change status` and `pnpm version -r` [#​13345](https://github.com/pnpm/pnpm/issues/13345).
- `pnpm self-update` no longer takes any instruction from the project it is run in:
- pnpm is fetched through the same trusted registry and auth configuration used when switching pnpm versions, so a project `.npmrc` or `pnpm-workspace.yaml` can no longer redirect the download or attach credentials to it, and the project's default `.pnpmfile.(c|m)js` is no longer loaded. Pnpmfiles from trusted sources (the `pnpmfile` setting, the global pnpmfile, config dependencies) still apply.
- The `minimumReleaseAge` settings in `pnpm-workspace.yaml` no longer affect `self-update`. They still govern the project's own dependencies; for `self-update` the cooldown now comes from the built-in default, your global config, a `PNPM_CONFIG_*` environment variable, or a command-line flag. This fixes `self-update` failing inside a workspace that raises the cutoff while succeeding everywhere else, and stops a repository from either waiving the cooldown or keeping you on an outdated pnpm by raising it.
- The same applies to the `trustPolicy` settings and to `ci`: a project can no longer weaken the trust check that guards the pnpm download, nor re-enable the confirmation prompt that a CI run suppresses.
When `self-update` refuses a version that is younger than the cutoff, an interactive run now offers to update anyway; non-interactive runs still fail. CI never prompts, even on a runner that attaches a TTY.
#### Patch Changes
- Fixed `pnpm licenses list` to report every version when the same package is installed under multiple aliases [pnpm/pnpm#13438](https://github.com/pnpm/pnpm/issues/13438).
- Sort `pnpm dedupe --check` snapshot changes for stable output across pnpm implementations.
- Strip Unicode formatting characters from registry- and manifest-derived terminal output.
- Speed up installs after compatible catalog or direct dependency range changes by retaining the locked version without resolving the dependency graph again.
- Speed up installs after safe override changes by reusing unambiguous compatible dependency resolutions, pruning obsolete dependencies, applying independent replacements and removals together, and handling parent-scoped `"-"` overrides without full lockfile resolution.
- Installing a local `file:` directory dependency with the global virtual store enabled no longer fails with `TypeError: Cannot read properties of undefined (reading 'split')` [#​13335](https://github.com/pnpm/pnpm/issues/13335).
Local directory dependencies — `file:` directories and injected workspace packages — now get a global-virtual-store slot of their own per project. They used to share one slot across every project that depended on a directory of the same name, so a project could end up linked to another project's copy of the dependency.
- The `Workspace` column of `pnpm update --interactive` now falls back to the project's path when its `name` is only whitespace, as it already did for a missing or empty one — all three render an equally blank label otherwise.
- Checking GitHub Actions dependencies for updates is now opt-in for every command. Neither `pnpm outdated` nor `pnpm update` reads the workflow files unless `--include-github-actions` is passed or `update.githubActions` is set to `true` in `pnpm-workspace.yaml`. Reading them runs `git ls-remote` against every referenced repository, which fails in environments where GitHub is not reachable the way pnpm assumes (a GitHub Enterprise Server, a custom certificate authority, or an offline network) [#​13254](https://github.com/pnpm/pnpm/issues/13254).
`pnpm outdated` accepts the `--include-github-actions` option too.
- `pnpm update --interactive` now measures its table in terminal columns rather than in characters. A package name, workspace name, or version containing wide characters (CJK, most emoji) no longer knocks its row's columns out of line with the rest of the group, and a wide character in a version no longer aborts the command with `Subject parameter value width cannot be greater than the container width` [#​13357](https://github.com/pnpm/pnpm/issues/13357).
- The `Workspace` column of `pnpm update --interactive` is more informative in two cases. A dependency outdated at the same version in several workspace projects is offered as one choice, since selecting it updates every project — that choice now names all of them instead of only the first. And a workspace project without a `name` is now labelled with its path rather than left blank, so several unnamed projects can be told apart.
- An auto-installed *optional* peer is no longer hoisted at a version the workspace root's own dependency on that package excludes. `resolvePeersFromWorkspaceRoot` already made the workspace root's specifier decide which version a missing *required* peer is installed at; the optional-peer picker ignored it and always took the highest version present anywhere in the graph. In a workspace whose root pins `postcss: 8.5.10`, an importer that depends on `webpack` and declares no `postcss` of its own got `postcss@8.5.22` hoisted for `terser-webpack-plugin`'s optional `postcss` peer, leaving two `postcss@8.5.x` instances in the graph [#​13320](https://github.com/pnpm/pnpm/issues/13320).
- `overrides` now also govern peers that pnpm auto-installs. Previously an override only rewrote dependencies declared in a manifest, so a peer nobody declares — installed because `autoInstallPeers` is on — resolved against its declared peer range and could bring in a second copy of the very package the override pinned. For example, with `overrides: { react: npm:react@19.2.0 }` and a lone `lucide-react` dependency, pnpm installed `react@18.3.1`; it now installs the pinned `react@19.2.0` [#​13320](https://github.com/pnpm/pnpm/issues/13320).
- Under `resolvePeersFromWorkspaceRoot`, a workspace root dependency declared with `link:` or `file:` (or the path form of `workspace:`, such as `workspace:../pkg`) now satisfies another project's missing peer dependency at the linked package's own version, instead of being hoisted as a path. Those specifiers are relative to the project that declares them, so the same specifier reached a different directory — or none — from the project the peer was hoisted into, leaving a broken link. The root now has the same authority over the peer as it has when it declares the package with a version range [#​13373](https://github.com/pnpm/pnpm/issues/13373).
- Installs through a pnpr server now apply the project's whole verification policy. `minimumReleaseAgeExclude`, `minimumReleaseAgeIgnoreMissingTime`, `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`, and `trustLockfile` were ignored, so excluded packages were still held back and a lockfile containing them could be rejected.
`trustPolicy: no-downgrade` no longer fails with `TRUST_POLICY_INCOMPATIBLE_WITH_PNPR` when a pnpr server is configured.
`--frozen-lockfile` and `--no-prefer-frozen-lockfile` are now honored on the pnpr path, instead of resolving and rewriting the lockfile anyway. Since `frozenLockfile` defaults to `true` on CI, a CI install through a pnpr server now fails on an out-of-date lockfile rather than updating it.
- Workspace installs through a pnpr server no longer crash with `Cannot read properties of undefined (reading 'filter')` after linking, when `minimumReleaseAge` is active [#​13275](https://github.com/pnpm/pnpm/issues/13275).
- Fixed `pnpm dedupe` updating valid catalog resolutions when another matching version exists in the lockfile.
- `pnpm -r run "/pattern/" --no-bail` no longer exits zero when one of a project's matched scripts fails and a later one passes. The run summary carries a single status per project, and the passing script overwrote the recorded failure.
- Restored the store block a first install prints, naming how packages were materialized and where the stores live [#​13315](https://github.com/pnpm/pnpm/issues/13315):
```text
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: ~/.local/share/pnpm/store/v11
Virtual store is at: node_modules/.pnpm
```
- The root project's `pnpm:devPreinstall` script now runs before resolution and linking, as it does in pnpm 11. It is skipped under `--ignore-scripts`, `--lockfile-only` and `--dry-run`, by `pnpm fetch` and `pnpm rebuild`, and by a repeat install that is already up to date. Workspaces that use the hook to prepare state the install depends on — such as [next.js](https://github.com/vercel/next.js), which generates a placeholder `next` bin with it — were left with dependents linked against files that were never created [#​13313](https://github.com/pnpm/pnpm/issues/13313).
- Prevented `pnpm dedupe --check` from removing an incompatible `node_modules` directory.
- `pnpm update --workspace` no longer links dependencies the user never named:
- Running it with `updateConfig.ignoreDependencies` configured no longer fails with `ERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUND` for a dependency that is only published to the registry. Such dependencies keep their specifiers, as they already did when no dependencies were ignored.
- Passing package selectors that match no direct dependency no longer falls back to linking every workspace dependency.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.17.0`](https://github.com/pnpm/pnpm/releases/tag/v11.17.0): pnpm 11.17
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.16.0...v11.17.0)
#### Minor Changes
- Added a new setting, `update.githubActionsServer`, for specifying the base URL of the GitHub server that hosts the repositories of the GitHub Actions referenced by the workflow files (for example, a GitHub Enterprise Server). When the setting is not defined, the URL is read from the `GITHUB_SERVER_URL` environment variable, falling back to `https://github.com`. The URL must use the `https://` or `http://` protocol [#​13220](https://github.com/pnpm/pnpm/issues/13220).
`pnpm outdated` and `pnpm update` no longer fail when the refs of a GitHub Action's repository cannot be read (for example, when the action's repository is private or hosted on a different GitHub server). Such actions are now skipped with a warning.
Setting `update.githubActions` to `false` now makes `pnpm outdated` and the interactive `pnpm update` skip GitHub Actions dependencies.
#### Patch Changes
- The token poll for web-based authentication no longer reads the body of non-OK or still-pending (HTTP 202) responses, and caps the token response body it does read at 64 KiB, so a malicious or compromised registry cannot exhaust memory through the poll [pnpm/pnpm#12721](https://github.com/pnpm/pnpm/issues/12721).
- Fixed `catalog:` references in dependencies and overrides failing to resolve when installing through a pnpr server, which errored with "No catalog entry '<name>' was found for catalog 'default'." even though the catalog entry existed. Also fixed a crash on Windows when installing a nested workspace member (e.g. `packages/foo`) through a pnpr server [#​13232](https://github.com/pnpm/pnpm/issues/13232).
- Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug [#​13164](https://github.com/pnpm/pnpm/issues/13164).
- Revert script ordering change for `pnpm run --sequential /regex/`
- Support the `from-git` argument in the `pnpm version` command.
- When the authentication URL cannot be rendered as a QR code (for example when it exceeds the maximum QR data capacity), web-based login now displays the URL alone with a warning instead of aborting authentication [pnpm/pnpm#12721](https://github.com/pnpm/pnpm/issues/12721).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.16.0`](https://github.com/pnpm/pnpm/releases/tag/v11.16.0): pnpm 11.16
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.15.1...v11.16.0)
#### Minor Changes
- The first release of a package now publishes the version written in its manifest verbatim, instead of bumping off it. `pnpm version -r` and `pnpm change status` check the registry for each release's current version; when that version is not yet published, the package debuts at it and its pending changesets apply only from the next release. A newly added package seeded at `1100.0.0` with a `minor` changeset is therefore published as `1100.0.0` rather than skipping straight to `1100.1.0`.
- Added a `--changeset` flag to `pnpm update`. Set `update.changeset` to `true` in `pnpm-workspace.yaml` to enable this behavior by default, and use `--no-changeset` to override the setting for one update. After the update completes, pnpm writes a `.changeset/pnpm-update-<suffix>.md` file declaring a patch bump for every workspace package whose `dependencies` or `optionalDependencies` were changed by the update and a major bump when `peerDependencies` changed, including packages that consume an updated catalog entry via the `catalog:` protocol. Private packages, packages without a name, and packages listed in the `ignore` array of `.changeset/config.json` are skipped. If `.changeset/config.json` does not exist, a warning is printed and no changeset is generated.
- Added GitHub Actions dependencies to `pnpm outdated` and interactive `pnpm update`. Non-interactive updates can include them with `--include-github-actions` or by setting `update.githubActions` to `true` in `pnpm-workspace.yaml`. Updated actions are pinned to exact commit hashes with their release tags preserved in comments.
- Added `update` and `audit` settings sections to `pnpm-workspace.yaml`, superseding the awkwardly named `updateConfig`, `auditConfig`, and top-level `auditLevel` settings:
```yaml
update:
ignoreDeps: # was updateConfig.ignoreDependencies
- webpack
- "@babel/*"
audit:
level: high # was auditLevel
ignore: # was auditConfig.ignoreGhsas
- GHSA-xxxx-yyyy-zzzz
```
`update.ignoreDeps` lists dependency name patterns that `pnpm update` and `pnpm outdated` should skip. `audit.level` and `audit.ignore` tune `pnpm audit`.
The deprecated `updateConfig`, `auditConfig`, and `auditLevel` settings keep working until the next major version. When both a new section value and its deprecated counterpart are set, the new section takes precedence and a warning is printed. Both the TypeScript CLI and the Rust config surface (pacquet) recognize the new sections.
#### Patch Changes
- Fixed `pnpm add --save-exact`/`--save-prefix` and `pnpm update` writing a package's version with the `peerDependencies` range's prefix (e.g. `^19.2.7` instead of the requested `19.2.7`) whenever the same package also appeared in `peerDependencies`. A real `dependencies`/`devDependencies`/`optionalDependencies` entry now takes precedence over a same-named `peerDependencies` entry when computing the current specifiers [#​13108](https://github.com/pnpm/pnpm/issues/13108).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.15.1`](https://github.com/pnpm/pnpm/releases/tag/v11.15.1): pnpm 11.15.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.15.0...v11.15.1)
#### Patch Changes
- `pnpm install` now detects a `supportedArchitectures` change and re-evaluates previously skipped platform-specific optional dependencies, instead of reporting the project as up to date and leaving the packages for the old architecture set in place.
- `pnpm setup` now removes leftover v10-layout shims at the top of `PNPM_HOME`, so `pnpm self-update` no longer warns about a v10 installation layout after PATH has been migrated to the v11 `PNPM_HOME/bin` layout. Applies to both the TypeScript CLI and pacquet.
In the TypeScript CLI, `self-update` also no longer treats a dangling legacy shim (one whose install target was garbage-collected) as a real v10 layout, so the warning can no longer fire on dead shim files.
Closes [#​12496](https://github.com/pnpm/pnpm/issues/12496).
- Completed pnpm runtime installation parity for Node.js, Deno, and Bun, including runtime failure policy, target architecture selection, and dependency runtime engines. Runtime failure overrides now preserve explicit runtime dependencies without matching engine entries.
- Fixed `pnpm install` running out of memory while resolving large dependency graphs [#​8441](https://github.com/pnpm/pnpm/issues/8441). The resolver kept full registry documents — per-version readmes, scripts, descriptions, and other install-irrelevant bulk — in memory for every package fetched with full metadata (optional dependencies, and packages re-fetched for `minimumReleaseAge`'s publish timestamps). Every retained document is now condensed down to the field set installation actually reads, which reduces peak resolution memory by several times on workspaces with more than a thousand packages.
- When a dependency's build script fails under `enableGlobalVirtualStore`, the global virtual store directory it was being built in is now removed for scoped packages too. Previously the cleanup resolved one directory level short of the hash directory for a scoped name, leaving a half-built directory behind that later installs would reuse.
- Fixed `pnpm login`, `pnpm adduser`, and `pnpm logout` against a registry hosted under a URL subpath (e.g. `https://example.com/npm/registry`) when the configured URL has no trailing slash. Such URLs were left unnormalized, so the last path segment was dropped when building the login and token endpoints and the auth token was stored under a truncated key. Registry URLs with a path now always get a trailing slash appended during normalization, matching how root-level registry URLs are handled.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.15.0`](https://github.com/pnpm/pnpm/releases/tag/v11.15.0): pnpm 11.15
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.14.0...v11.15.0)
##### Minor Changes
- Optional peer dependencies declared only via `peerDependenciesMeta` (for example `debug`'s `supports-color` peer) are now resolved from a satisfying version already present in the dependency graph, the same way explicitly declared optional peer dependencies are. Previously such peers were only resolved this way when the package's metadata was read back from the lockfile, so an unrelated dependency change could rewrite peer resolutions across the whole lockfile.
##### Patch Changes
- Updated `adm-zip` to prevent crafted ZIP archives from causing excessive memory allocation.
- `pnpm version -r` no longer writes a versioning-ledger entry with no consumed intents as a bare `intents:` key, which the next run failed to read with `ERR_PNPM_INVALID_VERSIONING_LEDGER`. Empty intent lists are now written as `intents: []`, and the ledger reader accepts the bare form left by earlier releases.
- Fixed pnpr workspace resolution to preserve project names and versions for `workspace:` dependencies.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.14.0`](https://github.com/pnpm/pnpm/releases/tag/v11.14.0): pnpm 11.14
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.13.1...v11.14.0)
#### Minor Changes
- `peerDependencies` now accept dependency specifiers that carry a scheme — a named-registry spec (`<registry>:<version>`), an `npm:` alias, or a `file:`/git/URL spec — instead of rejecting them with `ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION` [#​13095](https://github.com/pnpm/pnpm/issues/13095). Such a peer is matched against the semver range carried by the specifier (`work:5.x.x` is checked as `5.x.x`, `npm:bar@^5` as `^5`), or against `*` when it carries no version, while the original specifier still selects the package to auto-install. Bare `name@version` values, which are almost always a mistake, are still rejected.
- Added `pnpm doctor`, which diagnoses the pnpm installation and the environment it runs in: the versions and install method, whether the global bin directory is on `PATH`, whether the store and cache are writable, which link strategies (reflink, hardlink, symlink) the store's filesystem supports, registry connectivity, and an offline `file:` install that exercises the resolve/store/link path end to end. Each check reports how to fix what it finds, and the command exits non-zero when any check fails.
Use `--offline` to skip the checks that need network access, `--json` for machine-readable output, and `--benchmark` to time the filesystem and install checks.
- Added support for executing multiple scripts matching a RegExp passed to `pnpm run` (e.g., `pnpm run "/^build:.*/"`), running matched scripts in deterministic lexicographical order. Restored the `--sequential` (`-s`) CLI option for `pnpm run`, which forces `workspaceConcurrency` to 1 so that matched scripts run sequentially one by one across and within packages.
#### Patch Changes
- Fixed `pnpm install` failing with `ERR_PNPM_LOCKFILE_IS_SYMLINK` when `pnpm-lock.yaml` is a symlink, as build sandboxes such as Bazel and Nix stage it [#​13073](https://github.com/pnpm/pnpm/issues/13073). Reading a lockfile through a symlink is allowed again, and an install that leaves the lockfile unchanged no longer rewrites it, so `--frozen-lockfile` no longer needs to write at all. Writing a *changed* lockfile through a symlink is still refused, as that would redirect the write onto the symlink's target.
- Fixed frozen installs incorrectly treating equivalent Git dependency specifiers as a stale lockfile. See [#​13039](https://github.com/pnpm/pnpm/issues/13039).
- `pnpm owner ls` now reports authentication and authorization failures (401/403) as dedicated errors that include the registry's response body, matching `pnpm owner add`/`rm`, instead of a generic `Failed to fetch owners` message.
- Recover from a metadata cache entry that disappears (concurrent cache cleanup, antivirus) after the registry has already answered the conditional request with `304 Not Modified`. The metadata is re-requested once without cache validators instead of failing the install with `ERR_PNPM_CACHE_MISSING_AFTER_304`.
- A project pinned to a broken pnpm release via `packageManager` or `devEngines.packageManager` now reports which release is broken and what to do about it, instead of failing inside the installer. `pnpm self-update` already refused these releases; the version switch does too.
- Prevent broken-lockfile errors from including snippets of the lockfile's contents.
- `pnpm self-update` now checks that the version it installed can run before making it the active pnpm. A release that installs but cannot execute is discarded with an error instead of replacing a working installation.
- Fixed an out-of-memory regression when workspace projects concurrently resolve a package with large registry metadata [pnpm/pnpm#13077](https://github.com/pnpm/pnpm/issues/13077).
- Fixed `pnpm update` rewriting exact version pins that use the `=` operator (for example `=3.5.1`) to a caret range (`^3.5.1`). Exact pins are now preserved and written back as the bare version. See [#​12745](https://github.com/pnpm/pnpm/issues/12745).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.13.1`](https://github.com/pnpm/pnpm/releases/tag/v11.13.1): pnpm 11.13.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.13.0...v11.13.1)
#### Patch Changes
- Fixed `pnpm pack` applying workspace-root ignore rules when a workspace package has its own `.npmignore` file.
- Keep the interactive `minimumReleaseAge` approval prompt visible during `pnpm install`. The progress reporter now pauses its redraws while a prompt is waiting for input instead of overwriting it, so the install no longer hangs on a question the user cannot see [#​13019](https://github.com/pnpm/pnpm/issues/13019).
- Fixed `pnpm self-update` failing to link native platform binaries stored in sibling global virtual store slots.
### [`v11.13.0`](https://github.com/pnpm/pnpm/releases/tag/v11.13.0): pnpm 11.13
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.12.0...v11.13.0)
#### Minor Changes
- Added `versioning.epics` to `pnpm-workspace.yaml`. An epic ties a group of member packages to a lead package, constraining every member's major version to a band derived from the lead's major: while the lead is on major `M`, members live in `M*100 … M*100+99`. Members move independently inside the band (patch, minor, and a `major` intent that stays in-band); a bump that would carry a member past the band ceiling is rejected until the lead advances its own major. When a release plan takes the lead to a new stable major, every member re-bases to the band floor in the same plan. Membership is matched with pnpm's package selectors — name globs, `./`-prefixed directory globs, and `!`-prefixed negations.
- Added the `team` command for managing organization teams and team memberships on the registry, with create, destroy, add, rm, and ls subcommands and support for --otp, --parseable, and --json flags.
- Added native workspace release management [#​12952](https://github.com/pnpm/pnpm/issues/12952): the new `pnpm change` command records change intents as changesets-compatible `.changeset/*.md` files (`pnpm change status` shows the pending release plan), and the bare `pnpm version -r` consumes them — bumping versions across the workspace with dependent propagation through `workspace:` ranges, fixed groups, a `maxBump` cap, `--filter` narrowing, and `--dry-run` — writing changelogs, and recording consumed intents in a committed ledger that keeps cherry-picks and merge-backs between release branches safe. Packages can be moved onto per-package release lanes with the new `pnpm lane <name> --filter <pkg>` command and back with `pnpm lane main --filter <pkg>` (`pnpm lane` shows the membership), releasing `X.Y.Z-lane.N` prereleases from the same runs that release stable versions of the packages on the main lane. Configuration lives under the new `versioning` key of `pnpm-workspace.yaml` (`fixed`, `ignore`, `maxBump`, `lanes`, `changelog`). When two workspace projects publish the same name, intent files, `versioning.lanes`, and `versioning.fixed`/`ignore` may reference a project by its workspace-relative directory path (e.g. `"./pnpm/npm/pnpm"`) — the one additive extension to the changesets format, applied automatically by `pnpm change`.
Release changelogs default to `registry` storage (`versioning.changelog.storage`): no `CHANGELOG.md` is committed. Each release's section is composed at publish time and packed into the published tarball on top of the previously published version's changelog, and the consumed change intents are garbage-collected by a later `pnpm version -r` only once the registry confirms the version is published with its section. Set `versioning.changelog.storage: repository` to keep committed `CHANGELOG.md` files instead.
- Added a new override selector form with an empty range — `"pkg@": "<version>"` — called a convergence override. It rewrites a dependency edge only when its exact version satisfies the edge's declared range, so compatible consumers converge on one version while incompatible consumers keep their own resolution — now and for any dependent added in the future [#​12794](https://github.com/pnpm/pnpm/issues/12794).
```yaml
overrides:
"form-data@": 4.0.6
```
The value must be an exact version. When a full resolution detects that every declared range also admits a newer version, pnpm warns that the override is stale and names the version to converge on. Previously an empty range in an override selector was undocumented and behaved like a bare (unscoped) override.
#### Patch Changes
- A `tokenHelper` set in the global pnpm `auth.ini` is no longer rejected as project-level configuration. The guard that blocks `tokenHelper` from a project `.npmrc` only treated `~/.npmrc` as a trusted source, so a helper written to `auth.ini` (for example by `pnpm config set`) failed on every command and could not even be removed with `pnpm config delete`. A `tokenHelper` in a workspace or project `.npmrc` is still rejected.
- `pnpm cache delete` now removes a package's metadata from every metadata cache directory (`metadata`, `metadata-full`, and `metadata-full-filtered`), instead of only the one the current resolution mode reads. Previously a package cached under a different mode (e.g. `metadata-full-filtered`) was left behind. Closes [#​12753](https://github.com/pnpm/pnpm/issues/12753).
- Fixed an injected workspace dependency (`injectWorkspacePackages: true`) incorrectly staying as `file:` instead of deduping back to `link:` when an unrelated, ordinary shared dependency resolved to a peer-suffixed variant for the target project's own copy but not for the injected occurrence. See [#​10433](https://github.com/pnpm/pnpm/issues/10433).
- `pnpm deploy` now supports workspaces that use catalogs.
- Fixed `pnpm deploy` with a shared lockfile so local `file:` tarball dependencies keep their package name in the generated deploy lockfile. This prevents warm-store deploys from failing with `ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE` when the tarball filename includes the version.
- Options that follow `create`, `exec`, or `test` appearing as a subcommand of another command are now parsed instead of being silently treated as positional parameters. For example, `pnpm team create @org:team --registry <url>` previously ignored the `--registry` option and sent the request to the default registry.
- `pnpm add -g`, `pnpm update -g`, `pnpm setup`, and the self-updater no longer fail with `ERR_PNPM_MISSING_TIME` when `trustPolicy: no-downgrade` or `resolutionMode: time-based` is set in the global config [#​12883](https://github.com/pnpm/pnpm/issues/12883). The decision to fetch full registry metadata now lives in one place, and the `no-downgrade` trust policy always requests full metadata (matching the self-updater), since the trust evidence it checks is missing from abbreviated metadata even on registries that include the `time` field.
- `pnpm list` and `pnpm why` no longer crash with `EMFILE: too many open files` when a project has a large number of unsaved dependencies (packages present in `node_modules` but not in the lockfile). The reads of those packages are now concurrency-limited.
- The published `pnpm` package no longer declares `dependencies` or `devDependencies`. Because the CLI bundles its runtime dependencies into `dist/node_modules`, those fields are dropped when packing, so `npm install` of the tarball no longer tries to resolve internal-only packages such as `@pnpm/test-ipc-server`. Closes [#​12955](https://github.com/pnpm/pnpm/issues/12955).
- Fixed `pnpm publish --otp` and `pnpm publish --batch --otp` to send the configured OTP to the registry.
- `pnpm publish` again sends the package's README to the registry as metadata, so registries can render it on the package page. The readme is always included in the published metadata (matching the npm CLI), while the `embed-readme` setting continues to control only whether the readme is written into the `package.json` inside the tarball. This restores the behavior that was lost when publishing became fully native. Closes [#​12966](https://github.com/pnpm/pnpm/issues/12966).
- Fixed the dependency status check wrongly reporting "up to date" when a `package.json`, `.pnpmfile.cjs`, or patch file was edited in the same second as the previous install, on filesystems that record mtimes at whole-second resolution (for example ext4 with 128-byte inodes). The optimistic repeat-install fast path and `verify-deps-before-run` compared mtimes strictly, so a same-second edit whose mtime rounded down looked unchanged and re-resolution was skipped. Such a file's whole second is now treated as possibly-modified, falling through to the content check; behavior on sub-second filesystems is unchanged.
- Retry package metadata requests when a registry or proxy returns `304 Not Modified` to an unconditional request, preventing false `ERR_PNPM_CACHE_MISSING_AFTER_304` failures [pnpm/pnpm#12882](https://github.com/pnpm/pnpm/issues/12882).
If the retry also returns `304`, report `ERR_PNPM_META_NOT_MODIFIED_WITHOUT_CACHE` instead.
- Fixed `pnpm update` removing transitive lockfile entries when `dedupePeerDependents` is disabled and the selected package is absent [pnpm/pnpm#12456](https://github.com/pnpm/pnpm/issues/12456).
- Limit modern deploy lockfiles and localized virtual stores to dependencies reachable from the selected dependency groups.
- A `tokenHelper` command is now given a 60-second time limit. A helper that hangs (deadlock, stuck I/O) is killed and reported as an error instead of leaving the command waiting forever.
- Fixed orphaned child processes on Windows when pnpm exits on an error while commands spawned by `pnpm exec` or `pnpm dlx` are still running (for example, when one project's command fails during `pnpm --recursive exec`). The PIDs of these commands are now recorded when they are spawned and their whole process trees are terminated with `taskkill` on an error exit. Previously the cleanup relied on enumerating the system process list, which is so slow on Windows that the enumeration hit its timeout and the cleanup was silently skipped [#​12406](https://github.com/pnpm/pnpm/issues/12406).
- `pnpm pack` now respects workspace-root `.npmignore` and `.gitignore` files when packing workspace packages.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.12.0`](https://github.com/pnpm/pnpm/releases/tag/v11.12.0): pnpm 11.12
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.11.0...v11.12.0)
#### Minor Changes
- [`a897ef7`](https://github.com/pnpm/pnpm/commit/a897ef7): Custom fetchers exported from a pnpmfile can now delegate by returning a `{ delegate: <resolution> }` envelope: pnpm rewrites the package's resolution to the delegated shape and runs the built-in fetcher on it. This is the portable delegation form that also works in pacquet, where `cafs` and `fetchers` cannot be passed to the hook. Related to [pnpm/pnpm#11685](https://github.com/pnpm/pnpm/issues/11685).
#### Patch Changes
- [`2b02764`](https://github.com/pnpm/pnpm/commit/2b02764): The changed-packages filter (`--filter "...[<since>]"`) no longer allows an option-like `<since>` value (such as `--output=<path>`) to be interpreted as a git option — git now rejects it as a bad revision. The repository root is also resolved to the nearest `.git` entry, so the filter works in a git worktree checked out inside another repository's tree.
- [`43711ce`](https://github.com/pnpm/pnpm/commit/43711ce): `pnpm outdated` no longer checks the registry for dependencies that are resolved from local `link:`, `file:`, or `workspace:` references in the lockfile [#​12827](https://github.com/pnpm/pnpm/issues/12827).
- [`3c6718b`](https://github.com/pnpm/pnpm/commit/3c6718b): Fixed a deadlock in peer dependency resolution: `pnpm install` hung forever when a peer dependency cycle spanned a project's own dependencies and auto-installed peer providers, for example when installing `electron-builder@26.15.3` [#​12921](https://github.com/pnpm/pnpm/issues/12921).
- [`252f15e`](https://github.com/pnpm/pnpm/commit/252f15e): Fixed peer dependency auto-install picking a version the peer range rejects. In a workspace with several projects, a package declaring a peer dependency with a semver range (for example `^1.0.0`) could get the highest version found anywhere in the workspace (for example a `2.0.0` resolved for another project) instead of a version that satisfies the range. Peers are now deduplicated onto the highest preferred version that satisfies the declared range, and when none does, the range is resolved from the registry.
Also fixed re-resolving with an existing lockfile hoisting a different peer version than a fresh install of the same manifest: root dependencies reused from the lockfile were invisible to peer hoisting, so a peer that a root dependency provides could be bound to another version.
- [`a38adda`](https://github.com/pnpm/pnpm/commit/a38adda): `pnpm self-update <version>` now installs the requested pnpm version when it matches the currently running version but is missing from the global self-update directory.
- [`6a85968`](https://github.com/pnpm/pnpm/commit/6a85968): `pnpm stage list` now stops paginating after a fail-safe cap of 1000 pages, so a misbehaving registry cannot keep the command looping forever.
- [`eee7c9a`](https://github.com/pnpm/pnpm/commit/eee7c9a): `verify-deps-before-run` no longer spawns a `pnpm install` when pnpm is executed in a directory that has no `package.json`. A mistyped command run outside a project (for example `pnpm witch 10 login`) used to crash with a confusing error from the spawned install; now it fails with the regular "no package.json found" error.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.11.0`](https://github.com/pnpm/pnpm/releases/tag/v11.11.0): pnpm 11.11
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.10.0...v11.11.0)
#### Minor Changes
- [`508b8c2`](https://github.com/pnpm/pnpm/commit/508b8c2): Added the `pnpm access` command for managing package access and visibility on the registry, supporting listing packages and collaborators, getting and setting package status and MFA requirements, and granting or revoking team access.
#### Patch Changes
- [`c70e33e`](https://github.com/pnpm/pnpm/commit/c70e33e): Allow `allowBuilds` entries for git-hosted packages to match by repository URL without pinning the resolved commit hash. This lets trusted git repositories keep running their build scripts after branch updates without approving each new commit, while package-name-only rules still do not approve git-hosted artifacts.
- [`3067e4f`](https://github.com/pnpm/pnpm/commit/3067e4f): Reduced peak memory usage during cold-cache dependency resolution. The metadata fetch is memoized for the whole resolution phase, and it was retaining each package's raw registry response body (used only to mirror the response to disk) for that entire time. The memoized cache now holds a body-less copy, so the raw body only lives as long as the call that writes the disk mirror. On large graphs that fetch full metadata (e.g. with `minimumReleaseAge` or `trustPolicy` enabled) this cuts peak RSS by roughly 30%, back in line with pnpm 10. The resolved lockfile is unchanged.
- [`51300fd`](https://github.com/pnpm/pnpm/commit/51300fd): Prevent a crafted `pnpm-lock.yaml` from writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g. `../../../tmp/x@1.0.0`) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshot `version: "../../x"`) is now rejected at `formatGlobalVirtualStorePath`, the single point every global-virtual-store slot path funnels through — closing the same escape in the isolated linker, the resolver's dependency-graph builder, and the config-dependency installer.
- [`f8058eb`](https://github.com/pnpm/pnpm/commit/f8058eb): Reject symlinked `pnpm-lock.yaml` files when reading or writing the env lockfile document.
- [`9318a11`](https://github.com/pnpm/pnpm/commit/9318a11): Allow `registries` and `namedRegistries` to be configured in the global `config.yaml` file.
- [`51300fd`](https://github.com/pnpm/pnpm/commit/51300fd): Fixed a path traversal vulnerability where a dependency whose manifest `name` was a scoped path traversal (e.g. `@x/../../../<path>`) could be written outside `node_modules` to an attacker-controlled location during `pnpm install`, even with `--ignore-scripts`. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.
- [`14332f0`](https://github.com/pnpm/pnpm/commit/14332f0): Fail instead of silently removing an optional dependency's locked entries from `pnpm-lock.yaml` when the registry cannot resolve it. Previously, when registry metadata lacked a version that the lockfile already pinned (for example, a mirror that had not synced a recent release yet), `pnpm install` and `pnpm dedupe` silently dropped the optional dependency's entries — emptying maps such as the platform binaries of `@napi-rs/canvas` — so the lockfile differed between machines and frozen installs on other hosts had nothing to link [#​12853](https://github.com/pnpm/pnpm/issues/12853).
- [`fecfe83`](https://github.com/pnpm/pnpm/commit/fecfe83): Fixed peer dependency resolution with `autoInstallPeers` when a workspace package depends on a version of a package that a transitive dependency's self-contained closure also provides for itself. The peer providers that are attached to the root project for reuse are no longer peer-resolved a second time in the root context, so packages inside such a closure no longer get their peers bound to the root project's incompatible version [#​4993](https://github.com/pnpm/pnpm/issues/4993).
- [`5a4daec`](https://github.com/pnpm/pnpm/commit/5a4daec): `${...}` environment-variable placeholders in the `httpProxy`, `httpsProxy`, `noProxy`, `proxy`, and `noproxy` settings are no longer expanded when these settings come from a project's `pnpm-workspace.yaml`. They now receive the same protection already applied to `registry`, `namedRegistries`, and `pnprServer`.
- [`d1da02e`](https://github.com/pnpm/pnpm/commit/d1da02e): `pnpm publish` no longer prints credentials when the target registry is configured with inline `user:pass@` credentials (e.g. `registry=https://user:pass@example.com/`). They are now redacted both from the "publishing to registry" line and from the OIDC (trusted publishing) failure messages.
- [`dcfc611`](https://github.com/pnpm/pnpm/commit/dcfc611): `pnpm self-update` now honors `trustPolicy=no-downgrade`. It resolves the target pnpm version against full registry metadata, so it refuses to switch to a version whose supply-chain trust evidence is weaker than an earlier-published one, the same way a regular install does.
- [`a8ad82d`](https://github.com/pnpm/pnpm/commit/a8ad82d): Register the `pn` alias in generated shell completion scripts.
- [`25bd5c3`](https://github.com/pnpm/pnpm/commit/25bd5c3): Fixed standalone installer downgrades from pnpm v12 to v11.
- [`23996e9`](https://github.com/pnpm/pnpm/commit/23996e9): `pnpm runtime set <name> <version>` now validates its arguments: the name must be `node`, `deno`, or `bun`, and the version must not contain a comma. Previously these were interpolated straight into a `pnpm add` selector, where an unsupported name or a comma (e.g. `node 22,is-positive`) could be misread as a list of packages or a local directory and install unintended packages or bins.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.10.0`](https://github.com/pnpm/pnpm/releases/tag/v11.10.0): pnpm 11.10
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.9.0...v11.10.0)
#### Minor Changes
- [`e2e3c81`](https://github.com/pnpm/pnpm/commit/e2e3c81): Added the `issues` command as an alias of `bugs`, so `pnpm issues` opens the package's bug tracker URL in the browser.
- [`8491f8e`](https://github.com/pnpm/pnpm/commit/8491f8e): Added the `prefix` command which prints the current package prefix directory (or global prefix directory if `-g` / `--global` is used).
- [`3425e80`](https://github.com/pnpm/pnpm/commit/3425e80): Added an `_auth` setting for configuring registry authentication as a single structured (URL-keyed) value. It can be set in the **global** pnpm config (`config.yaml`) or, for CI, via the `pnpm_config__auth` environment variable. The env form sidesteps the GitHub Actions / bash / zsh limitation that broke the existing `pnpm_config_//host/:_authToken=…` form (env var names containing `/`, `:`, or `.` are silently dropped). Closes [#​12314](https://github.com/pnpm/pnpm/issues/12314).
The value is keyed by registry URL so each secret is explicitly bound to the host that may receive it. Registry URL keys must use `http` or `https` and must not include credentials, query strings, or fragments:
```sh
export pnpm_config__auth='{"https://registry.npmjs.org":{"@":{"authToken":"npm-token"},"@org":{"authToken":"org-token"}}}'
```
The equivalent in the global `config.yaml`:
```yaml
_auth:
https://registry.npmjs.org:
"@":
authToken: npm-token
"@org":
authToken: org-token
```
Within each registry URL, `@` means registry-wide/default credentials and package scopes like `@org` bind credentials to that scope on the same host. The only supported credential field is `authToken` (maps to `_authToken` / bearer auth); the deprecated `basicAuth` / `username` + `password` forms are intentionally not accepted here.
Each entry also infers a trusted registry route: `@` routes the default registry (and `pnpm add <pkg>` resolves there), and `@org` routes that scope. Because the credential and destination host arrive in one trusted value, repo-controlled `pnpm-workspace.yaml` or project `.npmrc` cannot redirect the token to a different host. `_auth` is honored **only** from the env var and the global config — it is ignored in a project `pnpm-workspace.yaml` / `.npmrc`, so repo-controlled config can never supply registry auth. Precedence: CLI flags (`--registry`, `--@scope:registry`) > `pnpm_config__auth` > global `config.yaml` `_auth` > `pnpm-workspace.yaml`.
Both `pnpm_config__auth` (lowercase, documented form) and `PNPM_CONFIG__AUTH` (all-caps, the shell convention some CI runners apply) are honored. If both are set, lowercase wins unless it is empty, in which case uppercase is used. The env var wins over the global `config.yaml` `_auth` on a conflicting key. `tokenHelper` is not supported in `_auth`. Parsing is strict: a malformed value (bad JSON, wrong shape, invalid registry URL or scope, an unsupported credential field) fails fast with an error rather than being silently dropped.
**Pacquet parity note:** the pacquet (Rust) port supports the same single credential field as the TS CLI: `authToken`.
- [`a33eeec`](https://github.com/pnpm/pnpm/commit/a33eeec): `pnpm self-update` and `packageManager` version-switching can now install and link pnpm v12 (the Rust port), published with equal content under both the `pnpm` and `@pnpm/exe` names on the `next-12` dist-tag. Its native binaries ship as `@pnpm/exe.<platform>-<arch>` packages, which pnpm's built-in installer links directly — no Node.js launcher, so the command pays no Node startup cost. v12 is initialized exactly like `@pnpm/exe`, including per-platform global-virtual-store hashing. From v12 onward the install converges on the unscoped `pnpm` package (the Rust exe) — even when updating from the SEA `@pnpm/exe` build.
- [`1dd12bd`](https://github.com/pnpm/pnpm/commit/1dd12bd): When resolving through a pnpr install-accelerator server, pnpm no longer forwards its own upstream registry credentials in the resolve request. Only the `Authorization` header identifying the caller to pnpr is sent. The pnpr server now selects upstream credentials from its own route policy (operator-configured upstream credential aliases), so private dependencies resolve through a pnpr-managed alias the caller is authorized to use, rather than by sending the client's registry tokens to the server.
- [`1e81761`](https://github.com/pnpm/pnpm/commit/1e81761): Expose web authentication `authUrl` and `doneUrl` in JSON error output when OTP is required in a non-interactive terminal [#​12724](https://github.com/pnpm/pnpm/issues/12724).
#### Patch Changes
- [`2f389d6`](https://github.com/pnpm/pnpm/commit/2f389d6): Added the Node.js release team's new signing key (Stewart X Addison, `655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD`) to the embedded Node.js release keys, so runtimes whose `SHASUMS256.txt` is signed by the new releaser verify successfully.
- [`acbdb94`](https://github.com/pnpm/pnpm/commit/acbdb94): Fixed shell tab completion not suggesting workspaces after the `-F` alias for `--filter` option.
- [`dcabb78`](https://github.com/pnpm/pnpm/commit/dcabb78): Fixed `pnpm up -r <pkg>` bumping unrelated packages that have open semver ranges. Previously, any update mutation nullified the lockfile-derived `preferredVersions` globally, so packages with `^x.y.z` ranges could re-resolve to newer compatible versions even though the user only asked to update a specific package. The install layer now always seeds `preferredVersions` from the lockfile, and caller-supplied preferred versions (such as the vulnerability penalties of `pnpm audit --fix`) layer on top of the seed instead of replacing it. The targeted package still bumps: the per-resolve `updateRequested` flag makes the resolver ignore the target's own lockfile pins.
Closes [#​10662](https://github.com/pnpm/pnpm/issues/10662).
- [`d539172`](https://github.com/pnpm/pnpm/commit/d539172): Fixed pnpm pack and pnpm publish failing when prepack generates files that are included in the package and postpack cleans them up.
- [`be6505a`](https://github.com/pnpm/pnpm/commit/be6505a): Hardened global package management:
- On Windows, removing or updating a global package now also cleans up the `node.exe` flavor of a bin, so a stale `node.exe` no longer survives on `PATH` after uninstall, and a new global install no longer silently overwrites an existing `node.exe`.
- `pnpm add -g pnpm@<version>` (and `@pnpm/exe@<version>`) is now rejected like the bare `pnpm` form, pointing to `pnpm self-update`.
- Dependency aliases read from a global package's manifest are validated before being joined onto `node_modules` paths, preventing a tampered manifest from escaping the install directory.
- Each global install group is created in its own freshly-made directory (no longer reusing a colliding or pre-existing path).
- Removing or updating a global package no longer unlinks a bin that belongs to a different globally installed package.
- [`25c7388`](https://github.com/pnpm/pnpm/commit/25c7388): pnpm now rejects `jsr:` specifiers whose package name is not a valid npm package name — an empty scope or name (e.g. `jsr:@scope/`), path separators inside the name, or any other shape `validate-npm-package-name` rejects — with `ERR_PNPM_INVALID_JSR_PACKAGE_NAME` instead of silently converting them into a malformed `@jsr/...` npm package name.
- [`25c7388`](https://github.com/pnpm/pnpm/commit/25c7388): pnpm now rejects named-registry specifiers (e.g. `gh:`) whose package name is not a valid npm package name — an empty scope (e.g. `gh:@/bar`), path separators inside the name (e.g. `gh:@scope/../name`), or any other shape `validate-npm-package-name` rejects — with `ERR_PNPM_INVALID_NAMED_REGISTRY_PACKAGE_NAME` instead of passing the name through to registry URLs and metadata cache file paths.
- [`96da7c5`](https://github.com/pnpm/pnpm/commit/96da7c5): node-gyp's `gyp_main.py` and `gyp` entrypoints are now packed with the executable bit in the `pnpm` and `@pnpm/exe` tarballs. Without it, building native addons from source could fail with a permission error.
- [`99982b9`](https://github.com/pnpm/pnpm/commit/99982b9): Sped up resolution and reduced memory use against registries that ignore npm's abbreviated metadata format and always return the full package document (for example, Azure DevOps Artifacts). pnpm now strips such documents down to the abbreviated field set before caching them. Resolution output is unchanged, and registries that honor the abbreviated format (such as the npm registry) pay no extra cost.
- [`11a7fdd`](https://github.com/pnpm/pnpm/commit/11a7fdd): Sped up offline and `--prefer-offline` resolution on large workspaces (e.g. `pnpm dedupe --offline`, `pnpm install --offline`). Package metadata loaded from the local cache is now kept in memory, so each package's metadata is parsed once per command instead of once per dependent that references it.
- [`2c7369d`](https://github.com/pnpm/pnpm/commit/2c7369d): `pnpm pack-app` now rejects `--entry` / `pnpm.app.entry` and `--output-dir` / `pnpm.app.outputDir` values that are absolute paths or escape the project directory via `..` (or a symlink that resolves outside it), and refuses to write the produced executable when its target path already exists as a symlink (or other non-regular file). This prevents a repository-controlled `package.json` from embedding host files (such as an SSH key) into the produced executable, writing build artifacts outside the project, or overwriting an arbitrary file through a committed symlink. The new error codes are `ERR_PNPM_PACK_APP_ENTRY_OUTSIDE_PROJECT`, `ERR_PNPM_PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT`, and `ERR_PNPM_PACK_APP_OUTPUT_FILE_NOT_REGULAR`.
When ad-hoc signing macOS targets, `pnpm pack-app` now runs the system `codesign` by absolute path and resolves `ldid` to a location outside the project, so a repository-controlled `node_modules/.bin` on `PATH` cannot hijack the signer.
- [`ce5d5a5`](https://github.com/pnpm/pnpm/commit/ce5d5a5): Relative paths in `patchedDependencies` are now resolved against the lockfile directory when computing patch file hashes, so running `pnpm install` from a subdirectory no longer fails with `ENOENT` looking for the patch file in the wrong location [#​12762](https://github.com/pnpm/pnpm/pull/12762).
- [`ebb4096`](https://github.com/pnpm/pnpm/commit/ebb4096): `pnpm peers` no longer reports a conflict for a missing peer dependency that is ignored via `pnpm.peerDependencyRules.ignoreMissing`.
- [`dcabb78`](https://github.com/pnpm/pnpm/commit/dcabb78): Fixed a prototype-pollution hazard when seeding preferred versions: a dependency named `__proto__` in a manifest or in `pnpm-lock.yaml` could write through `Object.prototype` (or crash the install) while the preferred-versions map was being built. The maps are now null-prototype objects, so crafted package names land as plain keys.
- [`f38e696`](https://github.com/pnpm/pnpm/commit/f38e696): Hardened `pnpm deploy --force` so it refuses unsafe deploy targets such as workspace roots, parent directories, out-of-workspace paths, and symlinked target parents.
- [`806c3ec`](https://github.com/pnpm/pnpm/commit/806c3ec): pnpm no longer warns about ignored project-level auth settings when `PNPM_CONFIG_NPMRC_AUTH_FILE` points at the project `.npmrc` — setting it to that file is an explicit opt-in to trusting it, so auth env variables in it are expanded [pnpm/pnpm#12480](https://github.com/pnpm/pnpm/issues/12480).
- [`991405e`](https://github.com/pnpm/pnpm/commit/991405e): Restore differential rendering (`ansi-diff`) to fix duplicated output lines introduced by [#​12351](https://github.com/pnpm/pnpm/issues/12351).
- [`c121235`](https://github.com/pnpm/pnpm/commit/c121235): Fixed the topological order of `--filter`ed commands (`pnpm run`, `pnpm exec`, `pnpm publish`, `pnpm pack`, `pnpm rebuild`) when the selected projects depend on each other only transitively through projects that were not selected. Previously such selected projects could run concurrently or in the wrong order; now a project always runs after the selected projects it transitively depends on, while projects without a real dependency relationship still run concurrently. This now also holds for prod-only filters (`--filter-prod`), which resolve order through the production dependency graph so transitive production dependencies are respected without pulling back the dev dependencies the filter drops, and for selections that mix `--filter` with `--filter-prod` [#​8335](https://github.com/pnpm/pnpm/issues/8335).
- [`d539172`](https://github.com/pnpm/pnpm/commit/d539172): `pnpm pack` and `pnpm publish` no longer follow a symlinked workspace `LICENSE` file when injecting it into a package that has no license of its own. Following the symlink could pack bytes from outside the workspace into the published tarball.
- [`dcabb78`](https://github.com/pnpm/pnpm/commit/dcabb78): Fixed `pnpm up <pkg>` producing a different result than a fresh install of the same manifests would. The resolver now distinguishes `updateRequested` (true only for packages that match the user's update target) from the broader `update` flag, and for the targeted package ignores only its own lockfile-derived preferred-version pins — so the target re-resolves exactly as if its lockfile entries were deleted and `pnpm install` ran. Preferred versions a fresh install applies (manifest pins, versions propagated down the dependency chain, and the vulnerability-avoidance penalties of `pnpm audit --fix`) stay in effect, so an update never installs duplicate versions that a reinstall from scratch would not reproduce. When a preferred version holds the update target below the newest version its range admits, pnpm now prints a warning explaining that reaching the newer version everywhere requires an override.
- [`dcabb78`](https://github.com/pnpm/pnpm/commit/dcabb78): `pnpm update <dep>@<version>` now prints a warning when `<dep>` is only present as a transitive dependency: the requested version cannot be applied there (updates resolve the target the way a fresh install would), and the warning recommends adding the version to `pnpm.overrides` instead, which is the mechanism that does pin transitive dependencies. Closes [#​12744](https://github.com/pnpm/pnpm/issues/12744).
- [`a6c4d5f`](https://github.com/pnpm/pnpm/commit/a6c4d5f): When a dependency cannot be found in the registry (404) or the registry has no matching version, and a workspace project with the same name exists only at non-matching versions, the error now reports the available workspace versions (`ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE`) instead of the raw registry failure [pnpm/pnpm#1379](https://github.com/pnpm/pnpm/issues/1379). Other registry failures (authorization, network, server errors) still propagate unchanged. The pacquet (Rust) resolver applies the same behavior.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.9.0`](https://github.com/pnpm/pnpm/releases/tag/v11.9.0): pnpm 11.9
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.8.0...v11.9.0)
#### Minor Changes
- [`bae694f`](https://github.com/pnpm/pnpm/commit/bae694f): Some registries generate tarballs on-demand and cannot provide an integrity checksum in their package metadata. In that case pnpm now computes the integrity from the downloaded tarball and stores it in the lockfile, so the entry is verifiable on subsequent installs instead of being written without an integrity (which would fail the next install). This also applies to `--lockfile-only`: the tarball is downloaded so its integrity can be computed. A lockfile entry that is still missing its integrity is rejected as a `ERR_PNPM_MISSING_TARBALL_INTEGRITY` lockfile verification violation (the install fails closed) rather than being silently re-fetched.
- [`6c35a43`](https://github.com/pnpm/pnpm/commit/6c35a43): Added `--exclude-peers` to `pnpm sbom`. With `auto-install-peers` (the default), peer dependencies resolve into the lockfile and are otherwise indistinguishable from the package's own dependencies. The flag drops peer dependencies (and any transitive subtree reachable only through them) from the SBOM. CycloneDX 1.7 has no scope or relationship that expresses "consumer-provided peer", so omission is the only spec-clean handling. The flag name matches `pnpm list --exclude-peers`; note the SBOM flag prunes a peer's exclusive subtree, which is stricter than `pnpm list` (which only hides leaf peers).
#### Patch Changes
- [`25a829e`](https://github.com/pnpm/pnpm/commit/25a829e): `pnpm audit --fix` now writes a single combined `minimumReleaseAgeExclude` entry per package (e.g. `axios@0.18.1 || 0.21.1`) instead of one entry per version, matching the format documented for the setting. Existing per-version entries in `pnpm-workspace.yaml` are merged into the combined form rather than left as duplicates. Installs that auto-collect immature versions into `minimumReleaseAgeExclude` now report the same combined entries, so the "Added N entries" message matches what is written to the manifest [#​12534](https://github.com/pnpm/pnpm/issues/12534).
- [`1cbb5f2`](https://github.com/pnpm/pnpm/commit/1cbb5f2): Fixed non-deterministic peer resolution that could add or remove an optional transitive peer — for example `@babel/core`, reached through `styled-jsx` — from a package's peer-dependency suffix across otherwise identical installs, churning the lockfile and causing intermittent `pnpm dedupe --check` failures in CI. When a package's children are resolved by one occurrence (the "owner") and reused by a deeper consumer, whether that consumer inherited the owner's missing peers depended on whether the owner's resolution had finished yet — a race under concurrent resolution. The decision is now a function of the dependency graph's structure rather than resolution-completion order.
- [`d577eea`](https://github.com/pnpm/pnpm/commit/d577eea): Fixed a Windows flakiness in `pnpm dlx` where a failed install could surface a spurious `EBUSY: resource busy or locked` error. The cleanup of a partially-populated dlx cache is now best-effort with retries and no longer masks the original error.
- [`ec7cf70`](https://github.com/pnpm/pnpm/commit/ec7cf70): Shortened the `pnpm dlx` cache path so deep dependency trees no longer overflow Windows' `MAX_PATH`, which could make a dependency's lifecycle script fail with `spawn cmd.exe ENOENT`.
- [`05b95ab`](https://github.com/pnpm/pnpm/commit/05b95ab): Fixed `pnpm` hanging (and crashing with an unhandled promise rejection) when a non-retryable network error such as `SELF_SIGNED_CERT_IN_CHAIN` occurs while fetching from a registry. The error is now rejected through the returned promise instead of being thrown inside the detached retry callback.
- [`d3f68e2`](https://github.com/pnpm/pnpm/commit/d3f68e2): Fix a `pnpm audit` performance regression on lockfiles that contain dependency cycles. The reachable-vulnerability pruning added in pnpm 11.5.1 only memoized acyclic subtrees, so any node whose subtree touched a cycle — together with all of its ancestors — was recomputed on every query, making the path walk quadratic. Reachability is now computed once per node using Tarjan's strongly-connected-components algorithm, so cyclic graphs are handled in linear time [#​12212](https://github.com/pnpm/pnpm/issues/12212).
The audit path walk also no longer recurses, so a deeply nested dependency graph can no longer overflow the call stack, and the install path to each finding is tracked without per-node copying, keeping memory linear in the graph depth.
- [`322f88f`](https://github.com/pnpm/pnpm/commit/322f88f): Fix failed optional dependency updates so they don't rewrite unrelated dependency specs [#​11267](https://github.com/pnpm/pnpm/issues/11267).
- [`1488db1`](https://github.com/pnpm/pnpm/commit/1488db1): When `enableGlobalVirtualStore` is toggled on for a project that was previously installed without it, stale hoisted symlinks under `node_modules/.pnpm/node_modules` are now replaced instead of being left pointing at the old per-project virtual store location [#​9739](https://github.com/pnpm/pnpm/issues/9739).
- [`6545793`](https://github.com/pnpm/pnpm/commit/6545793): Fixed `pnpm install --ignore-workspace` overwriting the `allowBuilds` map in `pnpm-workspace.yaml`. The ignored builds of a package with a build script were auto-populated into `allowBuilds` even though `--ignore-workspace` was passed, clobbering committed `true`/`false` values with the `set this to true or false` placeholder [#​12469](https://github.com/pnpm/pnpm/issues/12469).
- [`fbdc0eb`](https://github.com/pnpm/pnpm/commit/fbdc0eb): Fixed `minimumReleaseAgeExclude` and `trustPolicyExclude` so multiple exact-version entries for the same package behave the same as a single `||` disjunction entry. Previously only the first matching rule's versions were honored, so a config like `[form-data@4.0.6, form-data@2.5.6]` could still flag `form-data@2.5.6` as violating `minimumReleaseAge`, while `[form-data@4.0.6 || 2.5.6]` worked as expected [#​12463](https://github.com/pnpm/pnpm/issues/12463).
- [`fa7004b`](https://github.com/pnpm/pnpm/commit/fa7004b): The in-memory package metadata cache is now populated on the exact-version disk fast path, so repeated resolutions of the same package within one install no longer re-read and re-parse the on-disk metadata. In large monorepos this brings the time for adding a new package down from minutes to seconds. The in-memory cache key now also includes the registry, so a package of the same name served by two different registries in a single install can no longer share a cache slot and resolve the wrong tarball.
- [`0a154b1`](https://github.com/pnpm/pnpm/commit/0a154b1): Fixed `pnpm patch` dropping the package name (and leaking internal option fields) when the patched dependency resolves to a single git-hosted version.
- [`4d3fe4b`](https://github.com/pnpm/pnpm/commit/4d3fe4b): The pnpr resolver endpoints moved under the reserved `/-/pnpr` namespace: `POST /v1/resolve` is now `POST /-/pnpr/v0/resolve` and `POST /v1/verify-lockfile` is now `POST /-/pnpr/v0/verify-lockfile`. The capability handshake at `GET /-/pnpr` advertises protocol version `0` to match. This keeps every pnpr-proprietary route in npm's reserved namespace, so it can never collide with a package path.
- [`0ec878d`](https://github.com/pnpm/pnpm/commit/0ec878d): Removing a runtime dependency now removes the matching `devEngines.runtime` or `engines.runtime` entry that was materialized from it. Blank runtime selectors are normalized to `latest`.
- [`17e7f2c`](https://github.com/pnpm/pnpm/commit/17e7f2c): `pnpm sbom` now emits a CycloneDX `issue-tracker` external reference for components (and the root) whose `package.json` declares a `bugs` URL. Email-only `bugs` entries are skipped, since the reference requires a URL.
- [`a84d2a1`](https://github.com/pnpm/pnpm/commit/a84d2a1): Add `@pnpm/resolving.tarball-url`, which builds and recognizes the canonical npm tarball URL of a package. It vendors `getNpmTarballUrl` (previously the external `get-npm-tarball-url` package) and adds `isCanonicalRegistryTarballUrl`, the predicate the lockfile writer uses to decide whether a tarball URL is derivable from name+version+registry (and can therefore be omitted from `pnpm-lock.yaml`).
Exposing `isCanonicalRegistryTarballUrl` lets a custom resolver (pnpmfile `resolvers`) fronting a proxy that serves tarballs on a non-canonical path (e.g. an ephemeral `localhost:<port>`) rewrite the resolved tarball to the canonical form, so nothing host-specific is persisted to the lockfile. Previously this logic was private to `@pnpm/lockfile.utils`.
Two correctness fixes are included while consolidating the logic: the scoped-package unescape now handles uppercase `%2F` as well as `%2f` (percent-encoding is case-insensitive), and protocol-insensitive comparison strips only a leading `http(s)://` scheme instead of splitting on the first `://` (which could truncate URLs containing a later `://`).
- [`852d537`](https://github.com/pnpm/pnpm/commit/852d537): Lockfile verification no longer reports a registry metadata fetch failure (for example a `403`/`401` on a private registry, or a network error) as `ERR_PNPM_TARBALL_URL_MISMATCH`. When the registry can't be reached to verify an entry, the install now aborts with the registry's own fetch error (such as `ERR_PNPM_FETCH_403`, which already explains the authentication situation) instead of mislabeling a transport failure as lockfile tampering. Registry fetch errors no longer leak basic-auth credentials embedded in the registry URL (`https://user:pass@host/`) into their message.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.8.0`](https://github.com/pnpm/pnpm/releases/tag/v11.8.0): pnpm 11.8
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.7.0...v11.8.0)
#### Minor Changes
- [`c112b61`](https://github.com/pnpm/pnpm/commit/c112b61): Added a `--dry-run` option to `pnpm install`. It runs a full dependency resolution and reports what an install would change, but writes nothing to disk (no lockfile, no `node_modules`) and always exits with code 0. This mirrors the preview semantics of `npm install --dry-run` [#​7340](https://github.com/pnpm/pnpm/issues/7340).
- [`179ebc4`](https://github.com/pnpm/pnpm/commit/179ebc4): `pnpm run --no-bail` now exits with a non-zero exit code when any of the executed scripts fail, while still running every matched script to completion. This makes the exit-code behavior of `--no-bail` consistent between recursive and non-recursive runs (recursive runs already failed at the end). Previously, a non-recursive `pnpm run --no-bail` always exited with code 0, even when a script failed [#​8013](https://github.com/pnpm/pnpm/issues/8013).
- [`0474a9c`](https://github.com/pnpm/pnpm/commit/0474a9c): Added support for generating Node.js package maps at `node_modules/.package-map.json` during isolated and hoisted installs. Added the `node-experimental-package-map` setting to inject the generated map into pnpm-managed Node.js script environments, and the `node-package-map-type` setting to choose between `standard` and `loose` package maps.
- [`dcededc`](https://github.com/pnpm/pnpm/commit/dcededc): `pnpm sbom` now marks components reachable only through `devDependencies` with CycloneDX `scope: "excluded"` and the `cdx:npm:package:development` property. The `excluded` scope documents "component usage for test and other non-runtime purposes", which matches the semantics of a devDependency; the property is the CycloneDX npm-taxonomy marker emitted by `@cyclonedx/cyclonedx-npm`, so both modern (scope) and existing (property) consumers are covered. Components reachable at runtime (including installed `optionalDependencies`) omit `scope` and default to `required`.
- [`1495cb0`](https://github.com/pnpm/pnpm/commit/1495cb0): Added per-package SBOM generation with `--out` and `--split` flags. Use `--out out/%s.cdx.json` to write one SBOM per workspace package to individual files, or `--split` for NDJSON output to stdout. When `--filter` selects a single package, the SBOM root component now uses that package's metadata. Workspace inter-dependencies (`workspace:` protocol) and their transitive dependencies are included. Author, repository, and license fall back to the root manifest when the package doesn't define them.
- [`293921a`](https://github.com/pnpm/pnpm/commit/293921a): feat(view): support searching project manifest upward when package name is omitted
When running `pnpm view` without a package name, the command now searches
upward for the nearest project manifest (`package.json`, `package.yaml`, or `package.json5`) and uses its `name` field.
If the manifest exists but lacks a `name` field, an error is thrown.
This change also replaces the `find-up` dependency with `empathic` for
improved performance and consistency across workspace tools.
#### Patch Changes
- [`29ab905`](https://github.com/pnpm/pnpm/commit/29ab905): Fixed `pnpm update` overriding the version range policy of a named catalog whose name parses as a version (e.g. `catalog:express4-21`). The `catalog:` reference carries no pinning of its own, so the prefix from the catalog entry (such as `~`) is now preserved instead of being widened to `^` [#​10321](https://github.com/pnpm/pnpm/issues/10321).
- [`bee4bf4`](https://github.com/pnpm/pnpm/commit/bee4bf4): Security: validate config dependency names and versions from the env lockfile (`pnpm-lock.yaml`) before using them to build filesystem paths. A committed lockfile with a traversal-shaped `configDependencies` name (such as `../../PWNED`) or version (such as `../../../PWNED`) could previously cause `pnpm install` to create symlinks or write package files outside `node_modules/.pnpm-config` and the store. Names must now be valid npm package names and versions must be exact semver versions; the same validation is applied to optional subdependencies of config dependencies, and to the legacy workspace-manifest format before any lockfile is written. See [GHSA-qrv3-253h-g69c](https://github.com/pnpm/pnpm/security/advisories/GHSA-qrv3-253h-g69c).
- [`96bdd57`](https://github.com/pnpm/pnpm/commit/96bdd57): Fix `link:` workspace protocol switching to `file:` after `pnpm rm` is run from inside a workspace package whose target workspace dependency has its own dependencies, when `injectWorkspacePackages: true` is set. Follow-up to [#​10575](https://github.com/pnpm/pnpm/pull/10575), which fixed the same symptom for workspace packages without dependencies.
- [`302a2f7`](https://github.com/pnpm/pnpm/commit/302a2f7): No longer warn about using both `packageManager` and `devEngines.packageManager` when the two fields pin the same package manager at the same version with the same integrity hash (e.g. both `pnpm@11.5.1+sha512.…`). Previously the hash was stripped from the legacy `packageManager` field but not from `devEngines.packageManager`, so even identical specifications looked like a mismatch [#​12028](https://github.com/pnpm/pnpm/issues/12028).
The warning still fires on any genuine divergence, and several cases now state the specific reason instead of a single generic message: a different package manager, a different version, or contradictory integrity hashes for the same version.
- [`3f0fb21`](https://github.com/pnpm/pnpm/commit/3f0fb21): Fixed the progress line showing leftover characters from external processes that write to the terminal between progress updates (e.g. an SSH passphrase prompt would leave a fragment like `added 0sa':`). The interactive reporter now redraws each frame in place, erasing to the end of the display before reprinting, so any such remnants are cleared [#​12350](https://github.com/pnpm/pnpm/issues/12350).
- [`564619f`](https://github.com/pnpm/pnpm/commit/564619f): Fixed `pnpm approve-builds` reporting "no packages awaiting approval" when a build-script dependency whose approval was revoked (e.g. after `git stash` drops the `allowBuilds` from `pnpm-workspace.yaml`) is re-added. The revoked packages are now correctly recorded in `.modules.yaml` so `approve-builds` can find them. [#​12221](https://github.com/pnpm/pnpm/issues/12221)
- [`3d1fd20`](https://github.com/pnpm/pnpm/commit/3d1fd20): Skip the redundant "target bin directory already contains an exe called node" warning on Windows when the existing `node.exe` already matches the target (same hard link or identical content) [pnpm/pnpm#12203](https://github.com/pnpm/pnpm/issues/12203).
- [`1b02b47`](https://github.com/pnpm/pnpm/commit/1b02b47): Fix macOS Gatekeeper blocking native binaries (`.node`, `.dylib`, `.so`) by removing the `com.apple.quarantine` extended attribute after importing them from the store.
When pnpm imports files from its content-addressable store into `node_modules`, macOS preserves extended attributes, including `com.apple.quarantine`. If this xattr is present on a store blob (e.g. it was first written under a Gatekeeper-enabled app such as a Git client), it propagates to `node_modules`, and Gatekeeper blocks the native binary from loading even though pnpm already verified the file's integrity against the lockfile.
After importing a package, pnpm now strips `com.apple.quarantine` from its native binaries, matching Homebrew's behaviour of dropping quarantine from verified downloads. The cleanup is macOS-only, runs in a single batched `xattr` call per package, is restricted to native binaries (other files are untouched), and is non-fatal (it logs a warning on unexpected errors).
Fixes [#​11056](https://github.com/pnpm/pnpm/issues/11056)
- [`61969fb`](https://github.com/pnpm/pnpm/commit/61969fb): Fix `pnpm install` with `optimisticRepeatInstall` incorrectly reporting `Already up to date` when `pnpm-lock.yaml` changed but project manifests did not. This affected workflows such as checking out or restoring only the lockfile [#​12100](https://github.com/pnpm/pnpm/issues/12100).
Also fixes `checkDepsStatus` to use the correct lockfile path when `useGitBranchLockfile` is enabled, so the optimistic fast-path and lockfile modification detection work with `pnpm-lock.<branch>.yaml` files instead of always stat'ing `pnpm-lock.yaml`. Merge-conflict detection now reads the resolved lockfile name as well, and with `mergeGitBranchLockfiles` enabled every `pnpm-lock.*.yaml` is scanned for modifications and conflicts. The git branch is now resolved by reading `.git/HEAD` directly (no process spawn) and uses the workspace directory rather than `process.cwd()`.
- [`5c12968`](https://github.com/pnpm/pnpm/commit/5c12968): Fix recursive updates of transitive dependencies when the update command mixes transitive dependency patterns with direct dependency selectors. For example, `pnpm up -r "@babel/core" uuid` now updates matching transitive `@babel/core` dependencies even when `uuid` is a direct dependency selector [#​12103](https://github.com/pnpm/pnpm/issues/12103).
- [`9d79ba1`](https://github.com/pnpm/pnpm/commit/9d79ba1): Register the `pnpm update --no-save` flag in the CLI help and option parser.
- [`0474a9c`](https://github.com/pnpm/pnpm/commit/0474a9c): Fixed `pnpm import` for Yarn v2 lockfiles when `js-yaml` v4 is installed.
- [`9e0c375`](https://github.com/pnpm/pnpm/commit/9e0c375): Fixed `pnpm install` repeatedly prompting to remove and reinstall `node_modules` in a workspace package when `enableGlobalVirtualStore` is enabled. The post-install build step recorded a per-project `node_modules/.pnpm` virtual store directory in `node_modules/.modules.yaml`, overwriting the global `<storeDir>/links` value the install step had written. The next install then detected a virtual-store mismatch (`ERR_PNPM_UNEXPECTED_VIRTUAL_STORE`). The build step now derives the same global virtual store directory as the install step [#​12307](https://github.com/pnpm/pnpm/issues/12307).
- [`223d060`](https://github.com/pnpm/pnpm/commit/223d060): Document the `--cpu`, `--os` and `--libc` flags in the output of `pnpm install --help`. These flags were already supported but were only documented on the website [#​12359](https://github.com/pnpm/pnpm/issues/12359).
- [`e85aea2`](https://github.com/pnpm/pnpm/commit/e85aea2): Avoid reading `README.md` from disk when publishing if the publish manifest already provides a `readme` field. The README is now only read lazily, inside `createExportableManifest`, when it is actually needed.
- [`3188ae7`](https://github.com/pnpm/pnpm/commit/3188ae7): Fixed `pnpm peers check` to accept loose peer dependency ranges such as `>=3.16.0 || >=4.0.0-` when the installed peer version satisfies the range [#​12149](https://github.com/pnpm/pnpm/issues/12149).
- [`531f2a3`](https://github.com/pnpm/pnpm/commit/531f2a3): Fixed `pnpm update` rewriting a `workspace:` dependency that points at a local path (e.g. `workspace:../packages/foo/dist`) into a normalized `link:` or version-range specifier. Such specifiers are now preserved verbatim when the workspace protocol is preserved [#​3902](https://github.com/pnpm/pnpm/issues/3902).
- [`fe66535`](https://github.com/pnpm/pnpm/commit/fe66535): Fixed a lockfile non-convergence bug where an incremental install kept a duplicate transitive dependency that a fresh install would not produce. When a package is reused from the lockfile, its child edges are taken verbatim and bypass the preferred-versions walk, so a transitive dependency could stay pinned to an older version even after a direct dependency resolved to a higher version that satisfies the same range. The resolver now refreshes such a stale pin to the higher direct-dependency version during resolution — so the older version is never resolved or fetched, and the incremental result converges to the fresh one.
- [`6d35338`](https://github.com/pnpm/pnpm/commit/6d35338): `pnpm install` detects changes inside local file dependencies again. The optimistic repeat-install fast path only tracks manifest and lockfile modification times, so edits inside a local dependency's directory (or a repacked local tarball) were reported as "Already up to date". Projects with local file dependencies (`file:` and bare local path or tarball specifiers, declared directly or through `pnpm.overrides`) now always run a full install, which refetches those dependencies, matching pnpm v10 behavior [#​11795](https://github.com/pnpm/pnpm/issues/11795).
- [`4ca9247`](https://github.com/pnpm/pnpm/commit/4ca9247): Preserve the existing Node.js runtime version prefix when resolving `node@runtime:<range>` to a concrete version.
- [`30c7590`](https://github.com/pnpm/pnpm/commit/30c7590): Create shorter CAFS temporary package directories to leave room for lifecycle scripts that create IPC socket paths under TMPDIR.
- [`13815ad`](https://github.com/pnpm/pnpm/commit/13815ad): Reporter output (warnings, progress) for `pnpm store` and `pnpm config` subcommands now goes to stderr instead of stdout. This fixes scripts that capture their stdout (e.g. `PNPM_STORE=$(pnpm store path)`, `pnpm config list --json | jq`) from getting warnings mixed into the result.
- [`1c05876`](https://github.com/pnpm/pnpm/commit/1c05876): Avoid relinking unchanged child dependencies and remove stale child links during warm installs.
- [`817f99d`](https://github.com/pnpm/pnpm/commit/817f99d): Fixed lockfile churn where a package's `transitivePeerDependencies` could be dropped (and shift between packages) when the package participates in a dependency cycle. A cycle re-entry resolves against truncated children, so it must not be cached as "pure"; otherwise sibling occurrences of the same package short-circuit and lose transitive peers depending on traversal order [#​5108](https://github.com/pnpm/pnpm/issues/5108).
- [`eba03e0`](https://github.com/pnpm/pnpm/commit/eba03e0): Fix `pnpm install` reporting "Already up to date" after a catalog entry in `pnpm-workspace.yaml` was reverted to a previous version. After an update modified a catalog, the workspace state cache stored the pre-update catalog versions, so reverting the entry back to its original version was not detected as an outdated state [#​12418](https://github.com/pnpm/pnpm/issues/12418).
- [`3b54d79`](https://github.com/pnpm/pnpm/commit/3b54d79): `pnpm update` now keeps lockfile `overrides` that resolve through a catalog in sync with the catalog. Previously, when an override referenced a catalog (e.g. `overrides: { foo: 'catalog:' }`) and `pnpm update` bumped that catalog entry, the lockfile's `catalogs` advanced while the resolved `overrides` kept the old version. The resulting lockfile was internally inconsistent, so a later `pnpm install --frozen-lockfile` failed with `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`.
- [`9d0a300`](https://github.com/pnpm/pnpm/commit/9d0a300): Fixed `pnpm version --recursive` so it honors the workspace selection. In recursive mode the version bump now applies to the packages resolved from the workspace filter (`selectedProjectsGraph`), matching the behavior of `pnpm publish --recursive`, instead of always bumping every workspace package [#​11348](https://github.com/pnpm/pnpm/issues/11348).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.7.0`](https://github.com/pnpm/pnpm/releases/tag/v11.7.0): pnpm 11.7
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.6.0...v11.7.0)
#### Minor Changes
- Added a new setting `frozenStore` (`--frozen-store`) that lets `pnpm install` run against a package store on a read-only filesystem (e.g. a Nix store, a read-only bind mount, an OCI layer). When enabled, pnpm opens the store's SQLite `index.db` through the `immutable=1` URI — bypassing the WAL/`-shm` sidecar creation that otherwise fails on a read-only directory — and suppresses every store-write path (the `index.db` writer and the project-registry write). Pair it with `--offline --frozen-lockfile` against a fully-populated store. Under the global virtual store, package directories live inside the store, so if the store is missing the build output of a package whose lifecycle scripts are approved (or that has a patch), pnpm fails up front with `ERR_PNPM_FROZEN_STORE_NEEDS_BUILD` rather than crashing mid-build on a read-only write — seed the store with those builds first. Incompatible with `--force` and with a configured pnpr server, since both write into the store; the side-effects cache is likewise not written under `frozenStore`. If the store is missing its content directory, the install fails fast with `ERR_PNPM_FROZEN_STORE_INCOMPLETE` rather than attempting to initialize it. The read-only `immutable=1` open requires Node.js >=22.15.0, >=23.11.0, or >=24.0.0; on older runtimes `--frozen-store` fails with a clear `ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE` error. Bin-linking also tolerates a read-only store: under the global virtual store a package's bin source lives inside the store, so the `chmod` that makes it executable would be refused — with `EPERM`/`EACCES`, or with `EROFS` on a genuinely read-only filesystem. That `chmod` is redundant when the seed already ships its bins executable with a normalized shebang, so it is now skipped in that case, while a non-executable bin (or one still carrying a Windows CRLF shebang) on a read-only store still errors.
- When [`pacquet`](https://github.com/pnpm/pnpm/tree/main/pacquet) (the Rust port of pnpm) is declared in `configDependencies`, pnpm now delegates dependency **resolution** to it too — not just materialization — provided the installed pacquet is new enough to support full resolving installs (>= 0.11.7).
Previously pacquet only ran in frozen-install mode: pnpm always resolved the dependency graph itself (writing `pnpm-lock.yaml`) and handed pacquet a finished lockfile to fetch / import / link. With pacquet >= 0.11.7, a non-frozen `pnpm install` (default isolated `nodeLinker`, plain install) is delegated to pacquet end-to-end in a single pass — pacquet resolves the manifests, writes the lockfile, and materializes `node_modules`. pnpm detects the capability from the installed pacquet's version; older pacquet releases keep the resolve-then-materialize split, and `add` / `update` / `remove` still resolve in pnpm (it has to mutate the manifests first). This remains an opt-in preview of the Rust install engine [#​11723](https://github.com/pnpm/pnpm/issues/11723).
- Added a new opt-in `--batch` flag to `pnpm publish --recursive` that sends all selected packages to the registry in a single `PUT /-/pnpm/v1/publish` request instead of one request per package. The target registry has to implement the batch publish endpoint (pnpr does); registries that don't are reported with a clear `ERR_PNPM_BATCH_PUBLISH_UNSUPPORTED` error. The batch is processed all-or-nothing by pnpr: if any package in the batch fails validation, none of the packages are published.
#### Patch Changes
- Reject path-traversal and reserved dependency aliases (such as `../../../escape`, `.bin`, `.pnpm`, or `node_modules`) that come from a lockfile rather than a freshly resolved manifest. A crafted lockfile alias could otherwise be joined directly under a hoisted `node_modules` directory, letting package files be written outside the intended install root or overwrite pnpm-owned layout.
The fix adds two layers:
- The `nodeLinker: hoisted` graph builder now validates each alias at the directory sink (`safeJoinModulesDir`), matching the validation pnpm already performs when resolving aliases from manifests.
- The lockfile verification gate (`verifyLockfileResolutions`) now runs an always-on, policy-independent check that rejects any importer or snapshot dependency alias that is not a valid package name, failing the install early — before any fetch or filesystem work — for every node linker at once.
- Made shared package child resolution deterministic when the same package is reached through multiple contexts. pnpm now chooses the shallowest occurrence, then importer order, then parent path, instead of letting request timing decide the child context and missing-peer report [pnpm/pnpm#12358](https://github.com/pnpm/pnpm/issues/12358).
- Fix garbled summary line after submitting `pnpm update -i` and `pnpm audit --fix -i`. The interactive checkbox prompt previously printed every selected choice's full table row (label, current/target versions, workspace, URL) joined by commas, producing a wall of text after pressing Enter. The summary now lists only the selected package names (or vulnerability keys) by setting an explicit `short` per choice; the in-progress selection UI is unchanged.
- Prevent `pnpm patch-remove` from removing files outside the configured patches directory.
- Fixed `pnpm publish` ignoring `strictSsl: false` when publishing to registries with self-signed certificates. The `strictSSL` option is now forwarded to `libnpmpublish` / `npm-registry-fetch` so that `strict-ssl=false` in `.npmrc` or `strictSsl: false` in `pnpm-workspace.yaml` is respected during publish, the same way it is for `pnpm install` [pnpm/pnpm#12012](https://github.com/pnpm/pnpm/issues/12012).
- Fixed `Cannot destructure property 'manifest' of 'manifestsByPath[rootDir]' as it is undefined` regression introduced in 11.6.0 when running `pnpm add <pkg>` outside a workspace on Windows. `selectProjectByDir` was keying the resulting `ProjectsGraph` by `opts.dir` instead of `project.rootDir`, so downstream `manifestsByPath` lookups missed when the two paths normalized differently (typically drive-letter casing). [pnpm/pnpm#12379](https://github.com/pnpm/pnpm/issues/12379)
- Git dependencies that point to a subdirectory of a repository (`repo#commit&path:/sub/dir`) keep their `path` in the lockfile again. Since the integrity of git-hosted tarballs started being pinned in the lockfile, any install that actually downloaded the tarball rebuilt the lockfile resolution as `{ integrity, tarball, gitHosted }` and dropped the `path` field, while installs served from the store kept it — so the field disappeared seemingly at random. Without `path`, later installs from that lockfile silently unpacked the repository root instead of the subdirectory [#​12304](https://github.com/pnpm/pnpm/issues/12304).
- Fixed nondeterministic lockfile output that made `pnpm dedupe --check` fail intermittently in CI. When a locked peer provider was pinned for a dependency that has no child dependencies of its own, the pinned provider leaked into the shared parent scope, so siblings resolved after it could pick up an optional peer they should not see. Which siblings were affected depended on resolution order, which varies with network timing.
- Sped up `pnpm install` with a frozen lockfile by running lockfile verification (the policy revalidation gate added for `minimumReleaseAge`/`trustPolicy` and the tarball-URL anti-tamper check) concurrently with fetching and linking instead of blocking the whole install on it. Dependency lifecycle scripts are still held back until verification succeeds, so no script runs on an unverified lockfile: if verification fails the install aborts before any dependency build, and if linking finishes first the install waits for the verification verdict before completing.
- User-defined `npm_config_*` environment variables are now preserved during lifecycle script execution. Previously, all `npm_`-prefixed env vars were stripped, which caused user-set variables like `npm_config_platform_arch` to be lost [pnpm/pnpm#12399](https://github.com/pnpm/pnpm/issues/12399).
- pnpm can now use different auth tokens for different package scopes, even when those scopes use the same registry URL.
Previously, auth was selected only by registry URL. If `@org-a` and `@org-b` both used `https://npm.pkg.github.com/`, they had to share the same token. This caused problems for registries that issue tokens per organization or per scope.
Configure a scope-specific token by adding the package scope after the registry URL in the auth key:
```ini
@org-a:registry=https://npm.pkg.github.com/
@org-b:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:@org-a:_authToken=${ORG_A_TOKEN}
//npm.pkg.github.com/:@org-b:_authToken=${ORG_B_TOKEN}
//npm.pkg.github.com/:_authToken=${FALLBACK_TOKEN}
```
`pnpm login --registry=https://npm.pkg.github.com --scope=@org-a` writes the token to the same scope-specific auth key.
When installing or publishing `@org-a/*`, pnpm uses `ORG_A_TOKEN`. For `@org-b/*`, pnpm uses `ORG_B_TOKEN`. Packages without a matching scope continue to use the registry-wide fallback token.
- `pnpm setup` no longer prompts to approve build scripts for `@pnpm/exe` when installing the standalone executable. pnpm links the platform-specific binary itself, so the package's install scripts are skipped during the global self-install [#​12377](https://github.com/pnpm/pnpm/issues/12377).
- Close lockfile reads deterministically before rewriting lockfiles and keep pacquet's virtual store directory length aligned with pnpm on Windows.
- A `304 Not Modified` answer from the registry now renews the cached metadata file's mtime, so the `minimumReleaseAge` freshness shortcut keeps serving resolutions from the cache. Previously, once a cached packument grew older than `minimumReleaseAge`, every subsequent install re-validated it against the registry forever, because a 304 never rewrites the file.
- Updated dependency ranges. Notably:
- `@pnpm/logger` peer dependency range moved to `^1100.0.0`.
- `msgpackr` 1.11.8 → 2.0.4 (store index files remain byte-compatible in both directions).
- `open` ^7.4.2 → ^11.0.0, `memoize` ^10 → ^11, `cli-truncate` ^5 → ^6, `pidtree` ^0.6 → ^1.
- `@yarnpkg/core` 4.5.0 → 4.8.0, `@rushstack/worker-pool` 0.7.7 → 0.7.18, `@cyclonedx/cyclonedx-library` 10.0.0 → 10.1.0, `@pnpm/config.nerf-dart` ^1 → ^2, `@pnpm/log.group` 3.0.2 → 4.0.1, `@pnpm/util.lex-comparator` ^3 → ^4.
- Updated `@zkochan/cmd-shim` to v9.0.6.
- Fixed a Windows-only hang where a failed command could take 20–46 seconds to exit. On error, pnpm enumerates descendant processes (via `pidtree`) to terminate them, which on Windows shells out to `wmic`/PowerShell `Get-CimInstance Win32_Process` — a lookup that is extremely slow on some machines. The lookup is now bounded by a short timeout so it can no longer stall the process exit.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.6.0`](https://github.com/pnpm/pnpm/releases/tag/v11.6.0): pnpm 11.6
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.5.3...v11.6.0)
##### ⚠️ Security fix — environment variables in a project `.npmrc` (action may be required)
Following [GHSA-3qhv-2rgh-x77r](https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r), pnpm no longer expands `${ENV_VAR}` placeholders that come from a **repository-controlled** config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
- the project/workspace `.npmrc` — `registry`, `@scope:registry`, proxy URLs, URL-scoped keys (`//host/…`), and credential values (`_authToken`, `_auth`, `_password`, `username`, `tokenHelper`, `cert`, `key`);
- registry URLs in `pnpm-workspace.yaml`.
Environment variables are **still** expanded in trusted config: your user-level `~/.npmrc`, the global config, CLI options, and environment config.
**If your authentication broke after upgrading**, move the token out of the committed `.npmrc`:
```sh
# Writes to your user/global config, not the repository:
pnpm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
```
Or keep the `${NPM_TOKEN}` line but put it in your user-level `~/.npmrc` instead of the repo. In **GitHub Actions**, `actions/setup-node` with `registry-url` already writes a user-level `.npmrc`, so `NODE_AUTH_TOKEN` keeps working. For other CI where editing each pipeline is hard, set `PNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc` (or `NPM_CONFIG_USERCONFIG=.npmrc`) in the CI environment to declare the project `.npmrc` trusted.
See <https://pnpm.io/npmrc> for full migration details.
#### Minor Changes
- `pnpm install` completes without re-resolving when `pnpm-lock.yaml` was deleted but `node_modules` is intact: the up-to-date check now treats the current lockfile (`node_modules/.pnpm/lock.yaml`) — the record of what the previous install materialized — as the wanted lockfile, verifies the manifests still match it, restores `pnpm-lock.yaml` from it, and reports "Already up to date". Previously this scenario triggered a full resolution and a re-verification of every locked package against the registry.
- [`615c669`](https://github.com/pnpm/pnpm/commit/615c669): Added support for configuring URL-scoped registry settings through `npm_config_//…` and `pnpm_config_//…` environment variables, for example:
```text
npm_config_//registry.npmjs.org/:_authToken=<token>
pnpm_config_//registry.npmjs.org/:_authToken=<token>
```
This provides a file-free way to supply registry authentication. Because the registry a value applies to is encoded in the (trusted) environment variable name, it is host-scoped by construction and cannot be redirected to another registry by repository-controlled config. The environment value is treated as trusted config: it takes precedence over a project/workspace `.npmrc` but is still overridden by command-line options. When the same key is provided through both prefixes, `pnpm_config_` wins.
- Raised the default network concurrency from `min(64, max(cpuCores * 3, 16))` to `min(96, max(cpuCores * 3, 64))`. Package downloads are I/O-bound, not CPU-bound, so deriving the floor from the core count left machines with few cores (for example 4-vCPU CI runners) downloading only 16 tarballs at a time and unable to saturate a low-latency registry. The `networkConcurrency` setting still overrides the default.
#### Patch Changes
- Improved the warning printed when a project `.npmrc` uses an environment variable in a registry/proxy URL or in registry credentials. The message now explains why the setting was ignored and how to migrate it to a trusted source — for example by moving the line to the user-level `~/.npmrc` or running `pnpm config set "<key>" <value>` — with a link to <https://pnpm.io/npmrc>. The `pnpm config set` example is only suggested when the key has no `${...}` placeholder, so the snippet is always safe to copy-paste.
- Print a "Lockfile passes supply-chain policies (verified 2h ago)" message when lockfile verification is skipped because a cached verdict for the same lockfile content and policy is reused. Previously the cached short-circuit was completely silent, which made it look like the policy gate never ran [#​12324](https://github.com/pnpm/pnpm/issues/12324).
- Platform-specific optional dependencies are now skipped even when their `os`/`cpu`/`libc` fields are missing from the registry metadata or the lockfile. Some registries strip these fields from the package metadata, which made pnpm download and install the binaries of every platform regardless of `supportedArchitectures`. The missing platform fields of an optional dependency are now inferred from its name (e.g. `@nx/nx-win32-arm64-msvc` → `os: win32`, `cpu: arm64`), so foreign-platform binaries are skipped without even downloading them [#​11702](https://github.com/pnpm/pnpm/issues/11702).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.5.3`](https://github.com/pnpm/pnpm/releases/tag/v11.5.3): pnpm 11.5.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.5.2...v11.5.3)
##### ⚠️ Security fix — environment variables in a project `.npmrc` (action may be required)
Following [GHSA-3qhv-2rgh-x77r](https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r), pnpm no longer expands `${ENV_VAR}` placeholders that come from a **repository-controlled** config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
- the project/workspace `.npmrc` — `registry`, `@scope:registry`, proxy URLs, URL-scoped keys (`//host/…`), and credential values (`_authToken`, `_auth`, `_password`, `username`, `tokenHelper`, `cert`, `key`);
- registry URLs in `pnpm-workspace.yaml`.
Environment variables are **still** expanded in trusted config: your user-level `~/.npmrc`, the global config, CLI options, and environment config.
**If your authentication broke after upgrading**, move the token out of the committed `.npmrc`:
```sh
# Writes to your user/global config, not the repository:
pnpm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
```
Or keep the `${NPM_TOKEN}` line but put it in your user-level `~/.npmrc` instead of the repo. In **GitHub Actions**, `actions/setup-node` with `registry-url` already writes a user-level `.npmrc`, so `NODE_AUTH_TOKEN` keeps working. For other CI where editing each pipeline is hard, set `PNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc` (or `NPM_CONFIG_USERCONFIG=.npmrc`) in the CI environment to declare the project `.npmrc` trusted.
See <https://pnpm.io/npmrc> for full migration details.
#### Patch Changes
- Stopped expanding environment variables in repository-controlled registry/proxy request destinations and registry credential values from `.npmrc`, and in workspace registry URLs from `pnpm-workspace.yaml`. Move dynamic registry URL and token configuration to trusted user, global, CLI, or environment config.
- Resolve package-manager bootstrap dependencies with trusted user or CLI registry and network config, and reject package-manager env-lockfile records that do not use registry package paths with integrity-only resolutions before auto-switch execution.
- Avoid writing `packageManagerDependencies` to `pnpm-lock.yaml` when package manager policy is set to `onFail: ignore` or `pmOnFail: ignore` [#​12228](https://github.com/pnpm/pnpm/issues/12228).
- Avoid running dependency-status auto-install when the dependency status is unavailable without a project manifest.
- Using the `$` version reference syntax in `overrides` (e.g. `"react": "$react"`) now prints a deprecation warning. The syntax still works, but [catalogs](https://pnpm.io/catalogs) are the recommended way to keep an overridden version in sync with the rest of the workspace. Reference a catalog entry with the `catalog:` protocol instead.
- Fixed `pnpm config get globalconfig` to return the global `config.yaml` path again [pnpm/pnpm#11962](https://github.com/pnpm/pnpm/issues/11962).
- Fixed bare `--color` so it does not consume the following CLI flag, allowing command shorthands like `--parallel` to expand correctly and forms like `pnpm --color with current <command>` to dispatch the inner command instead of failing with `MISSING_WITH_CURRENT_CMD`.
- Fix `pnpm install` ignoring `enableGlobalVirtualStore` toggle by including it in the workspace state settings check [#​12142](https://github.com/pnpm/pnpm/issues/12142).
- Security: pnpm now verifies the npm registry signature of a package-manager binary before spawning it, so a cloned repository cannot make pnpm download and execute an arbitrary native binary.
This covers two paths that select an executable from repository-controlled input:
- **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` opts in to pnpm's Rust install engine. pnpm now verifies that the installed `pacquet` shim and the host's `@pacquet/<platform>-<arch>` binary carry a valid npm registry signature for their exact `name@version`, and refuses to run pacquet (failing the command) if the signature does not verify or cannot be checked. The only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.
- **automatic version switch / `self-update`** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. pnpm now verifies the registry signature of `pnpm`, `@pnpm/exe`, and the host platform binary before installing/spawning them, and refuses to run an engine whose signature does not match a published, signed release. The check runs only on an actual download (store cache miss), so it does not add a network round trip to every command.
In both cases the signature is verified over the *installed* integrity, against npm's public signing keys that ship embedded in the pnpm CLI (like corepack), so bytes substituted via a tampered lockfile or a repository-controlled registry fail verification — and a registry the user did not vouch for cannot supply its own signing keys. The signed packument is fetched from the configured registry, so an npm mirror works transparently. Verification fails closed: if it cannot be completed (for example, the registry is unreachable), the command fails rather than running an unverified binary. The embedded keys are kept current by a release-time check against npm's signing-keys endpoint.
- Made peer-dependent deduplication deterministic. When a peer-suffixed package variant was a subset of two or more mutually incompatible larger variants, the variant it collapsed into depended on the order importers were resolved in, which varies between machines. This could resolve the same workspace to different lockfiles on different platforms and make `pnpm dedupe --check` alternate between passing and failing.
- Reject invalid package names and versions from staged tarball manifests before deriving filenames for `pnpm stage download`.
- Clarified in CLI help that the pnpm store is trusted shared state and store integrity checks are corruption detection, not a tamper boundary for untrusted store writers.
- Reject reserved manifest `bin` names (`""`, `"."`, `".."`, and scoped forms such as `@scope/..`) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted.
- Require trusted package identity before package-name `allowBuilds` entries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as the `allowBuilds` key. Lockfile verification now rejects lockfiles where a registry-style dependency path (`name@semver`) is backed by a git, directory, or git-hosted tarball resolution (`ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`), so the dependency path is a reliable artifact identity by the time scripts can run.
- Security: pnpm now verifies the OpenPGP signature of a downloaded Node.js runtime's `SHASUMS256.txt` before trusting its integrity hashes.
When a repository requests a Node.js runtime (e.g. via `devEngines.runtime` / `useNodeVersion`), the download mirror is repository-configurable through `node-mirror:<channel>`. The integrity of the downloaded binary was only checked against `SHASUMS256.txt` fetched from that same mirror — a circular check that a malicious mirror could satisfy by serving a tampered binary together with a matching `SHASUMS256.txt`. pnpm then executes the binary (for example to run lifecycle scripts).
pnpm now fetches `SHASUMS256.txt.sig` and verifies the detached OpenPGP signature against the Node.js release team's public keys, which ship embedded in the pnpm CLI. A mirror that serves a tampered binary cannot also produce a valid signature, so the download fails to verify. The embedded keys are kept current by a release-time check against the canonical `nodejs/release-keys` list.
The musl variants from the hardcoded `unofficial-builds.nodejs.org` mirror are not repository-configurable and are signed by a different key, so they continue to be trusted over TLS.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://openai.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.5.2`](https://github.com/pnpm/pnpm/releases/tag/v11.5.2): pnpm 11.5.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.5.1...v11.5.2)
#### Patch Changes
- Peer dependency resolution now reuses the peer contexts already recorded in the lockfile when those providers are still present in the dependency graph and still satisfy the peer ranges. This avoids unnecessary peer-context rewrites during lockfile regeneration. Current manifest choices remain authoritative: a newly added, explicitly updated, or aliased direct provider, a changed nested provider, or a locked version that no longer satisfies the range still takes precedence.
- The lockfile verifier now checks that a registry entry pinning an explicit `tarball` URL points at the artifact the registry's own metadata lists for that `name@version`. Previously a tampered lockfile could pair a trusted `name@version` with an attacker-chosen tarball URL (and a matching integrity for those bytes), so the install fetched the attacker's bytes. A mismatch — or any entry that can't be confirmed against the registry — is rejected with `ERR_PNPM_TARBALL_URL_MISMATCH`. Non-registry resolutions (`file:`, git-hosted, etc.) and registry entries without an explicit tarball URL (the URL is reconstructed from name+version+registry, so it is inherently bound) are unaffected; non-standard registry tarball URLs (npm Enterprise, GitHub Packages) still pass because they match the metadata.
- Fix `pnpm update --recursive --lockfile-only <pkg>@<version>` crashing with `Invalid Version` when the catalog entry for `<pkg>` is a version range (e.g. `^21.2.10`) and `catalogMode` is `strict` or `prefer`. The catalog–version comparison now skips the equality check when either side is a range rather than passing a range to `semver.eq()`, so range specifiers fall through to the existing mismatch handling instead of throwing [#​11570](https://github.com/pnpm/pnpm/issues/11570).
- Avoided a Node.js crash when pnpm exits after network requests on Windows.
- Fixed packages being materialized into the virtual store without their root-level files (`package.json`, `LICENSE`, README, root entrypoints) when multiple `pnpm install` processes ran against the same store/workspace concurrently. The fast import path used to destructively empty the shared target directory, so a concurrent importer could wipe files another importer had already written; if the surviving files included the `package.json` completion marker, every later install treated the broken directory as complete and never repaired it. The fast path now imports directly only when it can create the target directory exclusively, and otherwise builds the package in a private temp directory and atomically renames it into place [#​12197](https://github.com/pnpm/pnpm/issues/12197).
- Fix dependency build scripts not running under the global virtual store (`enableGlobalVirtualStore`).
In a workspace install, dependency build scripts are deferred to a single `rebuild` pass (`buildProjects`). That pass resolved each package's location from the classic `node_modules/.pnpm/<depPathToFilename>` layout, which does not exist under the global virtual store — so native dependencies (e.g. packages using `node-gyp` / `prebuild-install`) were never built and failed to load at runtime (`Cannot find module .../build/Release/*.node`).
`buildProjects` now resolves the global-virtual-store projection directory (`<storeDir>/links/<hash>`, computed with the same graph hash the installer uses) when `enableGlobalVirtualStore` is set, and serializes concurrent builds of the same shared projection so parallel workspace projects don't race on the same directory.
- Don't promote a `runtime:` dependency (such as the Node.js version from `devEngines.runtime` or `pnpm runtime set`) into a catalog when `catalogMode` is `strict` or `prefer`. A `runtime:` dependency round-trips to `devEngines.runtime`, which only recognizes the `runtime:` protocol; cataloging it rewrote the manifest entry to `catalog:`, which broke that round-trip, stranded it in `devDependencies`, and left `devEngines.runtime` untouched.
- Skip lockfile `minimumReleaseAge`/`trustPolicy` verification for non-registry tarball protocols (for example `file:`), so local tarball dependencies are not incorrectly checked against npm registry metadata.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.5.1`](https://github.com/pnpm/pnpm/releases/tag/v11.5.1): pnpm 11.5.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.5.0...v11.5.1)
#### Patch Changes
- Improve `pnpm audit` performance by pruning non-vulnerable lockfile subtrees and stopping path enumeration once vulnerable findings reach the path cap.
- Avoid crashing when the workspace state cache is partially written or malformed.
- Set `npm_config_user_agent` for root lifecycle scripts during headless installs.
- Preserve the `integrity` field of a remote (non-registry) tarball dependency when its lockfile entry is rebuilt. Re-resolving such a dependency without re-fetching it (for example via `pnpm update`, or when another dependency changes) produced a resolution with no integrity — URL/tarball resolvers only learn the integrity after the tarball is downloaded — so the previously recorded integrity was dropped, making later installs fail with `ERR_PNPM_MISSING_TARBALL_INTEGRITY` [#​12067](https://github.com/pnpm/pnpm/issues/12067).
- Normalize a string `repository` field into the `{ type, url }` object form when creating the publish manifest, matching npm's behavior. Some registries (e.g. Gitea/Codeberg) reject a string `repository` with a 500 Internal Server Error during `pnpm publish` [#​12099](https://github.com/pnpm/pnpm/issues/12099).
- Preserve compatible optional peer versions already present in the lockfile when resolving dependencies.
- Fixed inconsistent resolution of a peer dependency that is shared through a diamond. When a package peer-depends on both another package and one of that package's own peer dependencies (for example `@typescript-eslint/eslint-plugin` peer-depends on both `@typescript-eslint/parser` and `typescript`, and `@typescript-eslint/parser` peer-depends on `typescript`), pnpm no longer reuses a hoisted instance of the shared peer that was resolved against a different version [#​12079](https://github.com/pnpm/pnpm/issues/12079).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.5.0`](https://github.com/pnpm/pnpm/releases/tag/v11.5.0): pnpm 11.5
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.4.0...v11.5.0)
#### Minor Changes
- Added a new `hoistingLimits` setting for `nodeLinker: hoisted` installs, mirroring yarn's `nmHoistingLimits`. It accepts `none` (the default — hoist as far as possible), `workspaces` (hoist only as far as each workspace package), or `dependencies` (hoist only up to each workspace package's direct dependencies). Originally proposed in [#​6468](https://github.com/pnpm/pnpm/pull/6468), closing [#​6457](https://github.com/pnpm/pnpm/issues/6457).
- Replaced `enquirer` with `@inquirer/prompts` for all interactive prompts. Fixes the `update -i` scrolling overflow bug where long choice lists were clipped in the terminal [#​6643](https://github.com/pnpm/pnpm/issues/6643).
**User-facing changes:**
- `pnpm update -i` / `pnpm update -i --latest`: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via `usePagination`
- `pnpm audit --fix -i`: Same scrolling fix for vulnerability selection
- `pnpm approve-builds`: Interactive build approval prompts updated
- `pnpm patch`: Version selection and "apply to all" prompts updated
- `pnpm patch-remove`: Patch removal selection updated
- `pnpm publish`: Branch confirmation prompt updated
- `pnpm login`: Credential prompts updated
- `pnpm run` / `pnpm exec` (with `verifyDepsBeforeRun=prompt`): Confirmation prompt updated
Vim-style `j`/`k` keys still work for up/down navigation in all interactive prompts.
**Internal:** The `OtpEnquirer` and `LoginEnquirer` DI interfaces changed from `{ prompt }` to `{ input }` / `{ input, password }` respectively. Plugins or custom builds that inject their own enquirer mock will need to update.
- Staged publishes are now recognized in the trust scale. When a package version's registry metadata carries an `approver` field, it is treated as the strongest trust evidence (ranked above trusted publishers and provenance attestations), since staged publishes require 2FA publish approvals. This prevents false-positive trust downgrade errors when moving from a staged publish to a lower trust level [#​11887](https://github.com/pnpm/pnpm/issues/11887).
#### Patch Changes
- Fix pnpm hanging during peer resolution when an aliased install pulls in transitive packages with mutual peer cycles at different depths in the dependency tree (for example, `pnpm i nuxt@npm:nuxt-nightly@5x`). Cycles whose members hit the `findHit` cache instead of running their own `calculateDepPath` are now short-circuited by sibling resolutions at the level where the cycle is detected, so the cached path promises no longer deadlock. [#​11999](https://github.com/pnpm/pnpm/issues/11999).
- Fix `pnpm dist-tag add` and `pnpm dist-tag rm` against npmjs.org failing without `--otp` with `[ERR_PNPM_UNAUTHORIZED] You must be logged in to set dist-tag … "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA."`. pnpm now sends `npm-auth-type: web` on dist-tag writes and surfaces the resulting OTP challenge through the existing browser-based 2FA flow (the same `withOtpHandling` helper used by `pnpm publish`), so the browser opens, the user authenticates, and the dist-tag is set on retry. `--otp=<code>` continues to work via the classic flow.
- Fix `minimumReleaseAgeExclude` handling in npm resolution fast paths so excluded packages do not get pinned to stale versions. Excludes are honored consistently during `publishedBy` metadata selection and cache-mtime shortcuts.
- Fix the `integrity` field being dropped from the lockfile entry of a remote (non-registry) https-tarball dependency when an unrelated package is installed afterwards. URL/tarball resolvers do not return an integrity (it is only known after the tarball is downloaded), so when such a dependency was reused from the lockfile without being re-fetched, its integrity was lost. It is now carried over from the existing resolution. With pnpm's lockfile-integrity hardening, the missing integrity made subsequent `--frozen-lockfile` installs fail with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. [#​12001](https://github.com/pnpm/pnpm/issues/12001).
- Skip dependency re-resolution when `pnpm-lock.yaml` is missing but `node_modules/.pnpm/lock.yaml` exists and still satisfies the manifest. `pnpm install` now reuses the materialized snapshot to regenerate `pnpm-lock.yaml` instead of walking the registry to rebuild it from scratch, turning the cache+node\_modules variation into a near-no-op for users who deleted the lockfile but kept the install [#​11993](https://github.com/pnpm/pnpm/issues/11993).
`--frozen-lockfile` still refuses to proceed when `pnpm-lock.yaml` is absent — the regenerated lockfile must be committed, so failing loudly is the correct behavior for CI.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.4.0`](https://github.com/pnpm/pnpm/releases/tag/v11.4.0): pnpm 11.4
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.3.0...v11.4.0)
#### Minor Changes
- Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, `pnpm install` (non-frozen) would log `ERR_PNPM_TARBALL_INTEGRITY`, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.
`pnpm install` now exits with `ERR_PNPM_TARBALL_INTEGRITY` and a hint pointing at the new opt-in flag.
The only opt-in is **`pnpm install --update-checksums`** — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.
`--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass.
- `pnpm runtime set <name> <version>` now saves the runtime to `devEngines.runtime` by default instead of `engines.runtime`. Pass `--save-prod` (or `-P`) to save it to `engines.runtime` instead [#​11948](https://github.com/pnpm/pnpm/issues/11948).
#### Patch Changes
- Fix a credential disclosure issue where an unscoped `_authToken` (or `_auth`, or `username` + `_password`, or `tokenHelper`) defined in one source — `~/.npmrc`, `~/.config/pnpm/auth.ini`, a workspace `.npmrc`, CLI flags, etc. — would be sent as an `Authorization` header to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (`cert`, `key`) so they aren't presented to a registry their author didn't choose.
pnpm now rewrites each unscoped per-registry setting (`_authToken`, `_auth`, `username`, `_password`, `tokenHelper`, `cert`, `key`) to its URL-scoped form at load time, using the `registry=` value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding `registry=` therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. `ca`/`cafile` are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally.
Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since `npm@9`, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g. `//registry.example.com/:_authToken=...` or `//registry.example.com/:cert=...`).
`@pnpm/network.auth-header`: removed the `defaultRegistry` parameter from `createGetAuthHeaderByURI` and `getAuthHeadersFromCreds`. Now that credentials are URL-scoped at load time, the merged `configByUri` never contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed.
- Fix `pnpm deploy` crashing with `ENOENT: ... lstat '<deployDir>/node_modules'` when `configDependencies` declares pacquet (`pacquet` or `@pnpm/pacquet`). The deploy directory never installs config dependencies, so the install engine they designate isn't on disk to invoke; the nested install now skips them.
- Reject git resolutions whose `commit` field is not a 40-character hexadecimal SHA before invoking `git`. A malicious lockfile could otherwise smuggle a value such as `--upload-pack=<command>` through `git fetch` / `git checkout`, which on SSH or local-file transports executes the supplied command.
- Limit concurrent project manifest reads while listing large workspaces to avoid `EMFILE` errors.
- Reject patch files whose `diff --git` headers reference paths outside the patched package directory. Previously a malicious `.patch` file added via a pull request could write, delete, or rename arbitrary files reachable by the user running `pnpm install`.
- Improve the log message that pnpm prints after auto-adding entries to `minimumReleaseAgeExclude` when `minimumReleaseAge` is set without `minimumReleaseAgeStrict`. The message previously referred to the internal "loose mode" terminology, which wasn't searchable in the docs; it now tells the user to set `minimumReleaseAgeStrict` to `true` if they want these updates gated behind a prompt instead [#​11747](https://github.com/pnpm/pnpm/issues/11747).
- Reject dependency aliases that contain path-traversal segments (such as `@x/../../../../../.git/hooks`) when reading them from a package manifest or symlinking them into `node_modules`. A malicious registry package could otherwise use a transitive dependency key to make `pnpm install` create symlinks at attacker-chosen paths outside the intended `node_modules` directory.
- Reject `pnpm-lock.yaml` entries whose remote tarball `resolution:` block is missing the `integrity` field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips `integrity:`) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under `--frozen-lockfile`. pnpm now fails closed at lockfile-read time with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. Git-hosted tarballs (`gitHosted: true` or a URL on codeload.github.com / bitbucket.org / gitlab.com) and `file:` tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.
- Validate `devEngines.runtime` and `engines.runtime` version ranges for `node`, `deno`, and `bun` when `onFail` is set to `error` or `warn`. Previously these settings only had an effect with `onFail: 'download'` — the `error` and `warn` modes silently did nothing [#​11818](https://github.com/pnpm/pnpm/issues/11818). Violations now throw `ERR_PNPM_BAD_RUNTIME_VERSION`.
- Require provenance before treating trusted publisher metadata as the strongest trust evidence.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.3.0`](https://github.com/pnpm/pnpm/releases/tag/v11.3.0): pnpm 11.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.2.2...v11.3.0)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Minor Changes
- Added `pnpm stage` with `publish`, `list`, `view`, `approve`, `reject`, and `download` subcommands for npm staged publishing.
- Added a new setting `trustLockfile`. When `true`, `pnpm install` skips the supply-chain verification pass that re-applies `minimumReleaseAge` / `trustPolicy='no-downgrade'` to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author. Defaults to `false`; verification stays on by default. Set in `pnpm-workspace.yaml`.
Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (`~4k` lockfile entries with `minimumReleaseAge` + `trustPolicy: no-downgrade` enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (`time`, per-version `_npmUser.trustedPublisher`, `dist.attestations.provenance`). The abbreviated-metadata cache is similarly projected to just the package-level `modified` field and the set of currently-listed version names. Fixes [#​11860](https://github.com/pnpm/pnpm/issues/11860).
- Implemented `pnpm pkg` command natively, following `npm pkg` standards.
- Implemented `pnpm repo` command natively, following `npm repo` standards.
- Implemented `pnpm set-script` (alias `ss`) natively. Adds or updates an entry in the `scripts` field of the project manifest, supporting `package.json`, `package.json5`, and `package.yaml` formats.
- Add a `skip-manifest-obfuscation` option for `pnpm pack` and `pnpm publish`. When enabled, the original `packageManager` field and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specific `pnpm` field continues to be omitted.
#### Patch Changes
- Fixed `pnpm dlx` failing with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` when the installed package's CAS slot is missing its `package.json`. Observed in the wild for `pnpm dlx node@runtime:<version>` when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship a `package.json` of their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, so `dlx` now falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including every `runtime:` spec) this matches what `manifest.bin` would have named. Multi-bin packages already require `--package=<spec> <bin>` to disambiguate and don't enter this code path.
- Fixed non-determinism in `pnpm dedupe` and `pnpm install` when a dependency graph contains packages with transitive peer dependencies on each other (e.g. `@aws-sdk/client-sts` and `@aws-sdk/client-sso-oidc`) and `auto-install-peers` is enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was that `resolveDependencies` pushed onto its `pkgAddresses` / `postponedResolutionsQueue` arrays from inside `Promise.all`-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes [#​8155](https://github.com/pnpm/pnpm/issues/8155).
- Fixed a regression introduced by [#​11711](https://github.com/pnpm/pnpm/pull/11711) where `pnpm add <github-shorthand>` (and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs or `pnpm/test-git-fetch#sha`) was silently dropped from the manifest update and from `pendingBuilds`. The alias-keyed lookup added in that PR couldn't find a `wantedDependency` whose `alias` was `undefined` at parse time but resolved to a package name only after fetching, so the entry never made it into `specsToUpsert`. Restored the original index-based pairing between `directDependencies` and `wantedDependencies`; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven by `rdd.catalogLookup.userSpecifiedBareSpecifier`, not by the lookup. Fixes the three `rebuilds dependencies` / `rebuilds specific dependencies` / `rebuild with pending option` failures in `building/commands/test/build/index.ts`.
- Fixed `pnpm add --config` leaving orphan entries in `pnpm-lock.env.yaml` (the optional subdependencies of the previously resolved version of the updated config dependency).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.2.2`](https://github.com/pnpm/pnpm/releases/tag/v11.2.2): pnpm 11.2.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.2.1...v11.2.2)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Minor Changes
- **Experimental:** Adding [`@pnpm/pacquet`](https://npmx.dev/package/@pnpm/pacquet) (the Rust port of pnpm) to `configDependencies` in `pnpm-workspace.yaml` now delegates the materialization phase of `pnpm install` to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine [#​11723](https://github.com/pnpm/pnpm/issues/11723).
To configure pacquet in a project, run:
```
pnpm add @pnpm/pacquet --config
```
You'll see changes in `pnpm-workspace.yaml` and `pnpm-lock.yaml` that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
#### Patch Changes
- When the install engine is delegated to pacquet via `configDependencies`, the user's CLI flags passed to `pnpm install` (e.g. `--no-runtime`, `--prod`, `--dev`, `--no-optional`, `--node-linker`, `--cpu`/`--os`/`--libc`, `--offline`, `--prefer-offline`) are now forwarded to pacquet's `install` subcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like `--no-runtime` were silently dropped. Flag forwarding is gated on the command being `install`/`i`; `add`, `update`, and `dedupe` still don't forward (their flag surface doesn't line up with pacquet's `install`).
- Fixed `pnpm up` (and `pnpm add` / `pnpm remove`) failing with `pacquet_package_manager::outdated_lockfile` when pacquet is declared in `configDependencies`. pnpm now passes `--ignore-manifest-check` to pacquet so its `--frozen-lockfile` check doesn't fire against the (pre-mutation) `package.json` pnpm hasn't written yet [#​11797](https://github.com/pnpm/pnpm/issues/11797). Requires a pacquet release that supports the flag — bump `PACQUET_VERSION` in the e2e tests once it ships.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.2.1`](https://github.com/pnpm/pnpm/releases/tag/v11.2.1): pnpm 11.2.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.2.0...v11.2.1)
#### Minor Changes
- **Experimental:** Adding [`@pnpm/pacquet`](https://npmx.dev/package/@pnpm/pacquet) (the Rust port of pnpm) to `configDependencies` in `pnpm-workspace.yaml` now delegates the materialization phase of `pnpm install` to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine [#​11723](https://github.com/pnpm/pnpm/issues/11723).
To configure pacquet in a project, run:
```
pnpm add @pnpm/pacquet --config
```
You'll see changes in `pnpm-workspace.yaml` and `pnpm-lock.yaml` that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
#### Patch Changes
- Mark optional subdependency snapshots of config dependencies with `optional: true` in the env lockfile, matching how optional dependencies are recorded elsewhere in `pnpm-lock.yaml`. Previously, snapshots for the platform-specific subdeps pulled in via a config dep's `optionalDependencies` were written as empty objects, which was inconsistent with the rest of the lockfile and made it look like those non-host platform variants were required.
- Fix `pickRegistryForPackage` returning the wrong registry for an unscoped `npm:` alias under a scoped local name. A manifest entry like `"@private/foo": "npm:lodash@^1"` was routing the `lodash` fetch through `registries["@private"]`, even though `lodash` is unscoped and doesn't live on that registry. The npm-alias branch now returns the alias target's own scope (or `null` for an unscoped target, falling through to `registries.default`) instead of leaking into the local key's scope.
- Don't print "Installing config dependencies..." when config dependencies are already installed and nothing needs to be fetched, re-linked, or removed.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.2.0`](https://github.com/pnpm/pnpm/releases/tag/v11.2.0): pnpm 11.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.1.3...v11.2.0)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Minor Changes
- **Experimental:** Adding [`@pnpm/pacquet`](https://npmx.dev/package/@pnpm/pacquet) (the Rust port of pnpm) to `configDependencies` in `pnpm-workspace.yaml` now delegates the materialization phase of `pnpm install` to the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine [#​11723](https://github.com/pnpm/pnpm/issues/11723).
To configure pacquet in a project, run:
```
pnpm add @pnpm/pacquet --config
```
You'll see changes in `pnpm-workspace.yaml` and `pnpm-lock.yaml` that should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.
- `configDependencies` now resolve and install one level of `optionalDependencies` declared by the config dependency, with `os`/`cpu`/`libc` platform filtering applied at install time. This unlocks the esbuild/swc-style pattern where a package ships platform-specific binaries via `optionalDependencies` — a config dependency can now do the same and have the matching binary symlinked next to it in the global virtual store, so `require('pkg-platform-arch')` from inside the config dependency resolves correctly.
The env lockfile records all platform variants regardless of host platform, so it remains portable across machines. Each entry in a config dependency's `optionalDependencies` must declare an exact version — ranges and tags are rejected to keep installs reproducible.
- Implement the documented `pnpm login --scope <scope>` flag. The scope is normalized (a leading `@` is added if missing; blank values are ignored) and an `@<scope>:registry=<registry>` mapping is written to the pnpm auth file alongside the auth token. Subsequent installs of `@<scope>/*` packages then route to the chosen registry. Previously `pnpm login --scope foo` errored with `Unknown option: 'scope'` despite the flag being listed in the online documentation [#​11716](https://github.com/pnpm/pnpm/issues/11716).
- `pnpm outdated` and `pnpm update --interactive` now report Node.js, Deno, and Bun runtimes installed as project dependencies (`runtime:` specifiers). Previously these were silently skipped.
#### Patch Changes
- Fix `cafile=<relative-path>` in `.npmrc` being read from the wrong directory when pnpm is invoked from a different cwd (e.g. `pnpm --dir <project> install` from a CI wrapper or monorepo script). The path is now resolved against the directory of the `.npmrc` that declared it, not `process.cwd()`. Before this fix the CA file silently failed to load — the install proceeded without the configured CA and the user only saw TLS errors against a private registry, with no log line tying back to the wrongly resolved path [#​11624](https://github.com/pnpm/pnpm/issues/11624).
- Fix `config.registry` getting a trailing slash appended when `registry` is set in `.npmrc` and no `registries.default` is provided by `pnpm-workspace.yaml`. The sync from `registries.default` to `config.registry` introduced in [#​11744](https://github.com/pnpm/pnpm/issues/11744) now only fires when the workspace manifest actually contributes a different default.
- Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error.
- Fix two crashes with `injectWorkspacePackages: true` when the lockfile has been pruned (e.g. by `turbo prune --docker`):
- `Cannot use 'in' operator to search for 'directory' in undefined`: a peer-dependency-variant injected snapshot inherits its `resolution` from the base `packages:` entry; when a pruner drops that base entry the readers crash. `convertToLockfileObject` now reconstructs the directory resolution from the `file:` depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot.
- `ERR_PNPM_ENOENT` on `node_modules/.bin/<tool>`: after `prepare`/`postinstall`, `runLifecycleHooksConcurrently` re-imported each injected workspace package; the `scanDir`-into-`filesMap` workaround fed target-internal paths to the importer, which the `makeEmptyDir` fast path ([#​11088](https://github.com/pnpm/pnpm/issues/11088)) then wiped. Drop the workaround and pass `keepModulesDir: true` so the importer preserves the target's existing `node_modules` (bin links + transitive deps) and source files keep their hardlinks.
- Fixed `pnpm login` and `pnpm logout` ignoring `registries.default` from `pnpm-workspace.yaml` [#​10099](https://github.com/pnpm/pnpm/issues/10099).
- Fix the `minimumReleaseAge` (publishedBy) maturity shortcut to be inclusive at the cutoff. Previously, abbreviated metadata whose `modified` field equalled the cutoff fell off the fast path and triggered a full-metadata re-fetch (or a `MISSING_TIME` error when full metadata wasn't permitted). Since `modified` is an upper bound on every version's publish time, `modified == publishedBy` already implies every version passes the per-version `<=` filter in `filterPkgMetadataByPublishDate`, so the shortcut now accepts the boundary case directly. Strictly `>` (was `>=`) at the rejection branch.
- Honor `publishConfig.access` when publishing packages.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.1.3`](https://github.com/pnpm/pnpm/releases/tag/v11.1.3): pnpm 11.1.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.1.2...v11.1.3)
#### Patch Changes
- `pnpm install` now re-validates `pnpm-lock.yaml` entries against the active `minimumReleaseAge` and `trustPolicy: 'no-downgrade'` policies before any tarball is fetched. Lockfiles resolved elsewhere (committed to the repo, restored from a CI cache, produced by an older pnpm) under a weaker or absent policy can no longer install a freshly-published or trust-downgraded version silently. Violating entries abort the install with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`, `ERR_PNPM_TRUST_DOWNGRADE`, or the generic `ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION` when both policies trip in the same batch; `minimumReleaseAgeExclude` and `trustPolicyExclude` are honored. Verification results are cached so repeat installs against an unchanged lockfile take a fast path, and pnpm shows a transient progress line while the registry round-trip runs.
When fresh resolution picks an immature version, the behavior depends on `minimumReleaseAgeStrict`:
- **Loose mode** — the default, in effect whenever `minimumReleaseAge` keeps its built-in 24-hour value — auto-adds the immature picks to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml` and lets the install proceed. A single info message lists what was persisted.
- **Strict mode** in an interactive terminal collects every immature direct AND transitive pick in one pass and prompts once with the full list. Approving adds them to `minimumReleaseAgeExclude` and the install continues; declining aborts before the lockfile, `package.json`, or `node_modules` is touched.
- **Strict mode** in CI (or any non-TTY context) aborts with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing every offending entry, instead of failing on the first one the resolver hit.
`minimumReleaseAgeStrict` auto-enables whenever the user explicitly sets `minimumReleaseAge` (CLI flag, env var, global `config.yaml`, or `pnpm-workspace.yaml`); set `minimumReleaseAgeStrict: false` to keep loose-mode auto-collect even with an explicit `minimumReleaseAge` value. Closes [#​10438](https://github.com/pnpm/pnpm/issues/10438), [#​10488](https://github.com/pnpm/pnpm/issues/10488), [#​11687](https://github.com/pnpm/pnpm/issues/11687).
- Allow redundant trailing base64 padding in `.npmrc` auth values and report invalid auth base64 with a pnpm error.
- Make `pnpm self-update` respect `minimumReleaseAge` (and `minimumReleaseAgeExclude`) when resolving which pnpm version to install.
When the `latest` dist-tag points to a version newer than the configured age threshold, `self-update` now selects the newest mature version instead unless excluded by `minimumReleaseAgeExclude`.
Also makes `dlx` and `outdated` surface invalid `minimumReleaseAgeExclude` patterns under the same `ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE` error code already used by `install`, instead of leaking the internal `ERR_PNPM_INVALID_VERSION_UNION` / `ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION` codes.
- Global installs respect global config build policy (e.g., `dangerouslyAllowAllBuilds` from config.yaml) when GVS is enabled [#​9249](https://github.com/pnpm/pnpm/issues/9249).
The global virtual-store (GVS) default `allowBuilds = {}` was applied before workspace manifest settings were read and before global config values (stripped by `extractAndRemoveDependencyBuildOptions`) were re-applied via `globalDepsBuildConfig`. This caused `hasDependencyBuildOptions` to return `true` (because `{}` is not null), blocking restoration of global config values like `dangerouslyAllowAllBuilds`. As a result, global installs skipped all build scripts even when the config explicitly allowed them.
This fix moves the GVS default to **after** workspace manifest reading and `globalDepsBuildConfig` re-application, so that:
1. Workspace manifest `allowBuilds` takes precedence (if present)
2. Global config `dangerouslyAllowAllBuilds` is properly restored (if set and no workspace policy exists)
3. Empty `{}` is only applied as a last resort when no policy is configured anywhere
- Honor `--silent` when `verifyDepsBeforeRun: install` auto-installs dependencies before `pnpm run` or `pnpm exec`, preventing install output from being written to stdout [#​11636](https://github.com/pnpm/pnpm/issues/11636).
- Fix lockfile parsing failures when `pnpm-lock.yaml` contains CRLF line endings and multiple YAML documents [#​11612](https://github.com/pnpm/pnpm/issues/11612).
- Anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime.
`ENGINE_NAME` (the `<platform>;<arch>;node<major>` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations:
1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`.
2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node.
Three changes:
- `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`.
- `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:<version>` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up.
- Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through.
On upgrade, two one-time GVS slot churns are possible:
- **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce.
- **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on.
In both cases the old slots become prune-eligible.
- Resolve the GVS hash's engine portion per-snapshot when a dependency declares its own `engines.runtime`, instead of using an install-wide value.
Pnpm's resolver desugars a dep's `engines.runtime` into `dependencies.node: 'runtime:<version>'`, and the bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into `<pkgDir>/node_modules/node/`. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so a pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose `<platform>;<arch>;node<major>` triple disagreed with the Node the build actually ran on.
Per-snapshot resolution now matches what `bins/linker` already does on a per-package basis:
- `@pnpm/deps.graph-hasher` adds `readSnapshotRuntimePin(children)` — reads the `node` entry from one snapshot's graph children and extracts the version from a `node@runtime:` value. Pairs with the existing `findRuntimeNodeVersion(snapshotKeys)` install-wide fallback (also now exported from `@pnpm/deps.graph-hasher` rather than `@pnpm/engine.runtime.system-node-version`, where it was a poor fit — `system-node-version` is about probing the host Node, not parsing lockfile-derived strings).
- `calcDepState` and `calcGraphNodeHash` consult `readSnapshotRuntimePin(graph[depPath].children)` first and only fall back to the install-wide `nodeVersion` parameter when the snapshot doesn't pin its own Node.
Pacquet mirrors the same precedence at the `calc_graph_node_hash` call site in `package-manager/src/virtual_store_layout.rs` — a new `find_own_runtime_node_major(snapshot)` helper reads each snapshot's `dependencies` for a `node` entry with `Prefix::Runtime` and overrides the install-wide engine when present.
On upgrade, snapshots of dependencies that declare their own `engines.runtime` re-hash under that dep's pinned Node instead of the install-wide value. The old slots become prune-eligible. Closes [#​11690](https://github.com/pnpm/pnpm/issues/11690).
- Fixed `pnpm publish` failing with a 404 when authentication relied on OIDC trusted publishing alongside an `.npmrc` written by `actions/setup-node` (`_authToken=${NODE_AUTH_TOKEN}`) without `NODE_AUTH_TOKEN` being set. Unresolved `${VAR}` placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source [#​11513](https://github.com/pnpm/pnpm/issues/11513).
- Fix `devEngines.packageManager` (singular form, without `onFail`) defaulting to `onFail: "error"` instead of the documented `pmOnFail: "download"`. As a result, a project that pinned a different pnpm version via `devEngines.packageManager` and ran `pnpm install` from a mismatched pnpm version failed with a hard error, even though the migration table from `managePackageManagerVersions: true` to `pmOnFail: download (default)` promises the install would auto-download the wanted version [#​11676](https://github.com/pnpm/pnpm/issues/11676).
The array form of `devEngines.packageManager` keeps its existing per-element defaults (`error` for the last entry, `ignore` for the rest), since those reflect explicit prioritization by the user. Explicit `onFail` values continue to win.
- Fix `devEngines.packageManager` not writing `packageManagerDependencies` to `pnpm-lock.yaml` when the lockfile lacks an env-doc entry. Previously the lockfile sync skipped resolution unless an existing `packageManagerDependencies.pnpm` entry needed refreshing, so a fresh install without `onFail: "download"` left the resolved pnpm version unrecorded — contradicting the documented behavior that the resolved version is stored in `pnpm-lock.yaml` [#​11674](https://github.com/pnpm/pnpm/issues/11674).
- Warn when `package.json` contains a legacy `pnpm` field with settings pnpm no longer reads from `package.json` (e.g. `pnpm.overrides`, `pnpm.patchedDependencies`). Previously these were silently ignored after the upgrade from v10, leaving users unaware that their overrides/patched dependencies had stopped taking effect [#​11677](https://github.com/pnpm/pnpm/issues/11677).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.1.2`](https://github.com/pnpm/pnpm/releases/tag/v11.1.2): pnpm 11.1.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.1.1...v11.1.2)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Patch Changes
- Strip `sec-fetch-*` headers from outgoing HTTP requests. These headers are automatically added by undici's `fetch()` implementation per the Fetch spec but cause Azure DevOps Artifacts to return HTTP 400 for uncached upstream packages, as ADO interprets them as browser requests [#​11572](https://github.com/pnpm/pnpm/issues/11572).
- Fix `minimumReleaseAge` handling for cached abbreviated metadata.
The version-spec cache fast path no longer rethrows `ERR_PNPM_MISSING_TIME` under `strictPublishedByCheck`; it now falls through to the registry-fetch path, consistent with the adjacent mtime-gated cache block.
When the registry returns 304 Not Modified for a package whose cached metadata is abbreviated (no per-version `time`), pnpm now re-fetches with `fullMetadata: true` if `minimumReleaseAge` is active and the package was modified after the cutoff. The upgraded metadata is persisted to disk so subsequent installs don't repeat the fetch. Previously the abbreviated meta was used as-is and the maturity check fell back to its warn-and-skip path, silently bypassing the quarantine and emitting a misleading "metadata is missing the time field" warning.
Closes [#​11619](https://github.com/pnpm/pnpm/issues/11619).
- Fix `pnpm upgrade --interactive --latest -r` not respecting named catalog groups. Previously, upgrading a dependency using a named catalog (e.g. `"catalog:foo"`) would incorrectly rewrite `package.json` to `"catalog:"` and place the updated version in the default catalog instead of the named one [#​10115](https://github.com/pnpm/pnpm/issues/10115).
- Fixed `optimisticRepeatInstall` skipping `pnpm-lock.yaml` merge conflict resolution when the existing `node_modules` state appears up to date.
- Fix `minimumReleaseAge` / `resolutionMode: time-based` installs failing on lockfiles whose `time:` block is missing entries. The npm-resolver's peek-from-store fast path now surfaces `publishedAt` from the lockfile rather than discarding it, and falls through to a registry metadata fetch when the time-based cutoff can't be computed from the data on hand.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.1.1`](https://github.com/pnpm/pnpm/releases/tag/v11.1.1): pnpm 11.1.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.1.0...v11.1.1)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Patch Changes
- Skip installability validation when scanning workspace projects in `checkDepsStatus` (run by `verifyDepsBeforeRun`). Previously the status check called `findWorkspaceProjects`, which validates each project's `engines` and `os`/`cpu`/`libc` and warns about useless fields in non-root manifests — work that the install pipeline already performs. With no `nodeVersion` threaded through, the engine check also fell back to the system Node from `PATH` and emitted spurious "Unsupported engine" warnings before scripts ran. Status-only callers now use `findWorkspaceProjectsNoCheck`; install paths continue to validate.
- Fixed `pnpm add <alias>:@scope/pkg` for [named registries](https://github.com/pnpm/pnpm/pull/11324). The local resolver was claiming any specifier containing `/` as a local directory, so `pnpm add bit:@teambit/bit` (with `bit` configured under `namedRegistries`) installed a bogus link to `bit:@teambit/bit/` instead of resolving from the configured registry. The local resolver now runs after the named-registry resolver in the resolution chain.
- Updated `@zkochan/cmd-shim` to 9.0.3. The sh shim it writes for `.cmd` / `.bat` targets now escapes the `/C` switch as `//C`, so it survives the path translation Git Bash applies when launching `cmd.exe`. Without this, a bare `/C` was rewritten to `C:\` before reaching cmd.exe — the switch was dropped, cmd started interactively, and the calling script saw the cmd banner instead of the wrapped command's output. Affects any cmd-shim-wrapped batch script invoked from Git Bash / MSYS / Cygwin on Windows. See [pnpm/cmd-shim#55](https://github.com/pnpm/cmd-shim/pull/55).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.1.0`](https://github.com/pnpm/pnpm/releases/tag/v11.1.0): pnpm 11.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.9...v11.1.0)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Minor Changes
- Added `pnpm audit signatures` to verify ECDSA registry signatures for installed packages against keys from `/-/npm/v1/keys` [#​7909](https://github.com/pnpm/pnpm/issues/7909). Scoped registries are respected, and registries without signing keys are skipped.
- Added support for installing packages from the [GitHub Packages npm registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry) via a built-in `gh:` prefix (e.g. `pnpm add gh:@acme/private`), and, more broadly, for arbitrary named registries in the style of [vlt's named-registry aliases](https://docs.vlt.sh/cli/registries). Authentication is picked up from the existing per-URL `.npmrc` entries (e.g. `//npm.pkg.github.com/:_authToken=...`), so no separate auth mechanism is required.
Additional aliases — or an override for the built-in `gh` alias, for GitHub Enterprise Server — can be configured under `namedRegistries` in `pnpm-workspace.yaml`:
```yaml
namedRegistries:
gh: https://npm.pkg.github.example.com/
work: https://npm.work.example.com/
```
With this, `work:@corp/lib@^2.0.0` resolves against `https://npm.work.example.com/`. [#​11324](https://github.com/pnpm/pnpm/issues/11324).
- Allow setting sbom spec version using `--sbom-spec-version` [#​11389](https://github.com/pnpm/pnpm/pull/11389).
- Add `--no-runtime` flag (config: `runtime=false`) to skip installing runtime entries (e.g. Node.js downloaded via `devEngines.runtime`) without modifying the lockfile. The lockfile keeps the runtime entry so frozen-lockfile validation still passes; only the runtime fetch and `.bin` linking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. via `pnpm runtime -g set node <version>`) before `pnpm install` runs.
- Added the `pnpm bugs` command that opens a package's bug tracker URL in the browser. With no arguments, it reads the current project's `package.json`; with one or more package names, it fetches each package's metadata from the registry and opens its bug tracker. Falls back to `<repository>/issues` when the `bugs` field is missing [#​11279](https://github.com/pnpm/pnpm/pull/11279).
- Added `pnpm owner` command to manage package owners on the registry.
#### Patch Changes
- Added "published X ago by Y" information to the `pnpm view` command output, similar to `npm view`. This is useful when comparing against `minimumReleaseAge`.
For example, `pnpm view pnpm` now shows:
```
published 17 hours ago by GitHub Actions
```
- `pnpm publish` now honors the configured HTTP/HTTPS proxy (including `https_proxy`/`http_proxy`/`no_proxy` environment variables) when polling the registry's `doneUrl` during the web-based authentication flow. Previously the poll bypassed the proxy, causing the registry to respond `403` from a different source IP and the login to never complete [#​11561](https://github.com/pnpm/pnpm/issues/11561).
- `pnpm add -g` now installs each space-separated package into its own isolated directory by default. To bundle multiple packages into the same isolated install (so that they share dependencies and are removed together), pass them as a comma-separated list. For example:
- `pnpm add -g foo bar` installs `foo` and `bar` as two independent globals — removing one does not affect the other.
- `pnpm add -g foo,bar qar` bundles `foo` and `bar` into a single isolated install while `qar` is installed on its own.
Related: [#​11587](https://github.com/pnpm/pnpm/issues/11587).
- `pnpm runtime set <name> <version>` no longer fails in the root of a multi-package workspace with the `ADDING_TO_ROOT` error. Installing the workspace root is a valid target for a runtime, so the command now bypasses that safety check.
- Fix `pnpm --version` hanging for the lifetime of the worker pool after the version was printed. `main.ts`'s `--version` short-circuit returned before reaching the command-handler `finally` that calls `finishWorkers()`, so the worker pool that `switchCliVersion` had spawned during integrity resolution stayed alive and held the Node event loop open. The CLI entry now runs `finishWorkers()` from its own `finally`, so every exit path tears the pool down.
Repro: `pnpm --version` in a workspace whose `devEngines.packageManager` version already matches the running pnpm + `onFail: "download"`. `switchCliVersion` resolves the integrity (spawning workers), finds nothing to swap, returns. The version prints, then the process hangs.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.9`](https://github.com/pnpm/pnpm/releases/tag/v11.0.9): pnpm 11.0.9
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.8...v11.0.9)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Patch Changes
- Fixed installation of GitLab-hosted dependencies. pnpm now downloads the tarball from `https://gitlab.com/<user>/<project>/-/archive/<sha>/<project>-<sha>.tar.gz` instead of the GitLab API endpoint that contained an encoded slash (`%2F`) between user and project. The encoded slash both triggered `406 Not Acceptable` responses from GitLab and produced virtual store directory names that Node refused to import (`ERR_INVALID_MODULE_SPECIFIER`) [#​11533](https://github.com/pnpm/pnpm/issues/11533).
- Honor `NPM_CONFIG_USERCONFIG` (and its lowercase `npm_config_userconfig` form) as a low-priority fallback when locating the user-level `.npmrc`. This restores compatibility with environments that point npm at a custom auth file via that env var — most notably `actions/setup-node`, which writes registry credentials to `${runner.temp}/.npmrc` and exports `NPM_CONFIG_USERCONFIG` to reference it. Without this, GitHub Actions workflows using `actions/setup-node` to authenticate to private registries broke after upgrading to pnpm v11. PNPM-prefixed env vars and `npmrcAuthFile` from the global `config.yaml` continue to take precedence [#​11539](https://github.com/pnpm/pnpm/issues/11539).
- Fix `pnpm pack` not bundling dependencies listed in `bundleDependencies` (or `bundledDependencies`). The npm-packlist upgrade in pnpm 11 changed its API to require the caller to pre-populate the dependency tree, which the wrapper was not doing — `bundleDependencies` were silently dropped from the tarball [#​11519](https://github.com/pnpm/pnpm/issues/11519).
- Fixed the pnpm CLI crashing with a confusing `SyntaxError: Invalid regular expression flags` instead of printing a clear "requires Node.js v22.13" error when launched on an unsupported Node.js version. The Node.js version check in `bin/pnpm.mjs` was effectively dead code because the static `import` of the bundled `dist/pnpm.mjs` was hoisted by the ES module loader and parsed before the check could run [#​11546](https://github.com/pnpm/pnpm/issues/11546).
- Fixed `pnpm --prefix=<dir> install` overwriting the existing `pnpm-workspace.yaml` in `<dir>` with `set this to true or false` placeholders. The renamed `--prefix` option (which maps to `dir`) was not honored when locating the workspace root, so the workspace manifest's `allowBuilds` settings were not loaded into config and got clobbered when ignored builds were auto-populated [#​11535](https://github.com/pnpm/pnpm/issues/11535).
- Fixed `pnpm publish --provenance` failing with a 422 from the registry when the package version contained semver build metadata (e.g. `1.0.0-canary.0+abc1234`). The `+<build>` segment is now stripped before packing so that the version embedded in the tarball, the metadata sent to the registry, and the sigstore provenance subject all agree [#​11518](https://github.com/pnpm/pnpm/issues/11518).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.8`](https://github.com/pnpm/pnpm/releases/tag/v11.0.8): pnpm 11.0.8
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.7...v11.0.8)
> See full [v11.0.0 changelog](https://github.com/pnpm/pnpm/releases/tag/v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Patch Changes
- Restored the heuristic that preserves tarball URLs in `pnpm-lock.yaml` when they cannot be derived from name+version+registry, even with the default `lockfileIncludeTarballUrl: false`. Without this, `pnpm install --frozen-lockfile` from an empty store fails with `ERR_PNPM_FETCH_404` for packages on registries that serve tarballs from a non-standard path — most notably GitHub Packages (`https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>`) and JSR. `lockfileIncludeTarballUrl: true` continues to force the URL into the lockfile for every package [#​11276](https://github.com/pnpm/pnpm/issues/11276).
- Run `preversion`, `version`, and `postversion` lifecycle scripts for `pnpm version`.
- Fixed `ERR_PNPM_BAD_TARBALL_SIZE` when a registry serves tarballs with an end-to-end `Content-Encoding` (e.g. `gzip`). Tarballs are already compressed, so the fetcher now requests them with `Accept-Encoding: identity` (matching pnpm v10's effective behavior) and, as defense in depth against misbehaving servers, no longer enforces the strict `Content-Length` check when the response declares a `Content-Encoding` — `Content-Length` in that case refers to the encoded payload, not the decoded bytes the fetch implementation yields [#​11506](https://github.com/pnpm/pnpm/issues/11506).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.7`](https://github.com/pnpm/pnpm/releases/tag/v11.0.7): pnpm 11.0.7
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.6...v11.0.7)
#### Patch Changes
- Restore the execute bit on the `node-gyp` shims packed inside `@pnpm/exe` (`dist/node-gyp-bin/node-gyp`, `dist/node-gyp-bin/node-gyp.cmd`, and `dist/node_modules/node-gyp/bin/node-gyp.js`). Without this, `pnpm/action-setup`'s standalone path (used on runners with Node.js < 22.13) failed any install whose lifecycle script invoked `node-gyp rebuild` with `sh: 1: node-gyp: Permission denied` [#​11483](https://github.com/pnpm/pnpm/issues/11483).
- Fixed the `pn`, `pnpx`, and `pnx` aliases failing in Git Bash / MSYS2 on Windows when pnpm was installed via `@pnpm/exe` (or after `pnpm self-update`) [#​11486](https://github.com/pnpm/pnpm/issues/11486). Running `pnpx` (or `pnx`) printed the cmd.exe banner and dropped the user into an interactive command prompt instead of running `pnpm dlx`. The `bin` field rewrite on Windows was pointing those aliases at `.cmd` files; cmd-shim's Bash shim for a `.cmd` target wraps it in `exec cmd /C ...`, and MSYS2 mangles `/C` into a Windows path before cmd.exe sees it. The aliases are now `.exe` hardlinks of the SEA binary, which detects which name it was launched as via `process.execPath` and prepends `dlx` for `pnpx` / `pnx`.
- Fix `pnpm install` recreating `node_modules` after `pnpm fetch`. `pnpm fetch` records empty `hoistPattern` and `publicHoistPattern` in `.modules.yaml`; since v11 removed the explicit-config gate, the follow-up install treated those as a hoist-pattern change and purged the modules directory. The fetch step now flags the modules manifest with `virtualStoreOnly: true` so the next install skips the hoist-pattern comparison and completes the missing post-import linking in place [#​11488](https://github.com/pnpm/pnpm/issues/11488).
- Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
A new `gitHosted: true` field is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.
- Allow user-level preferences in the global `config.yaml`. The following settings can now be set in `~/.config/pnpm/config.yaml` (or via `pnpm config set --location global`) instead of being restricted to `pnpm-workspace.yaml`: `agent`, `globalVirtualStoreDir`, `initPackageManager`, `initType`, `registrySupportsTimeField`, `scriptShell`, `shellEmulator`, `sideEffectsCache`, `sideEffectsCacheReadonly`, `stateDir`, `strictDepBuilds`, `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`, `updateNotifier`, `useStderr`, `verifyDepsBeforeRun`, `verifyStoreIntegrity`, `virtualStoreDir`, `virtualStoreDirMaxLength` [#​11474](https://github.com/pnpm/pnpm/issues/11474).
- Make trusted publishing (OIDC) take precedence over a configured static `_authToken` in `pnpm publish`, mirroring the npm CLI's behavior. When OIDC succeeds, the OIDC-derived token overrides any pre-configured `_authToken`; when OIDC is not applicable (no CI environment, exchange fails, registry has no trusted publisher configured), the static token is used as a fallback. This applies on every package during recursive publish, so each workspace package independently attempts trusted publishing.
Additionally, the `NPM_ID_TOKEN` env var is now honored as a CI-agnostic injection point for an OIDC ID token. Previously OIDC was only attempted on GitHub Actions or GitLab; now any CI provider that exposes its own OIDC mechanism (e.g. CircleCI's `CIRCLE_OIDC_TOKEN_V2`, Buildkite, etc.) can forward its token via `NPM_ID_TOKEN` and trusted publishing will work without pnpm needing to recognize the provider explicitly.
- `--pm-on-fail=ignore` (and other universal options like `--loglevel`, `--reporter`) is now honored when combined with `--help` or `--version`. Previously the CLI argument parser short-circuited those flags before universal options were preserved, so `pnpm audit --pm-on-fail=ignore --help` and `pnpm --pm-on-fail=ignore --version` reported the strict packageManager mismatch instead of running the requested action [#​11487](https://github.com/pnpm/pnpm/issues/11487).
- Fix a regression where `pnpm --recursive --filter '!<pkg>' run/exec/test/add` would include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative `--filter` arguments are provided, matching the [documented behavior](https://pnpm.io/cli/recursive). To include the root, pass `--include-workspace-root` [#​11341](https://github.com/pnpm/pnpm/issues/11341).
- Restore npm-CLI-compatible `--json` stdout output for `pnpm publish` ([#​11476](https://github.com/pnpm/pnpm/issues/11476)). pnpm 11 reimplemented publish natively ([#​10591](https://github.com/pnpm/pnpm/pull/10591)) and inadvertently dropped the per-package JSON object that pnpm 10 emitted transitively via the npm CLI, silently breaking downstream tooling — most notably `nx release publish`, which parses stdout JSON to confirm success ([nrwl/nx#35575](https://github.com/nrwl/nx/issues/35575)). On success, the output is now:
- `pnpm publish --json` → single object `{ id, name, version, size, unpackedSize, shasum, integrity, filename, files, entryCount, bundled }`, mirroring `npm publish --json`.
- `pnpm publish -r --json` → array of those objects, mirroring `pnpm pack --json`'s shape choice.
- `pnpm publish -r --report-summary` → existing `pnpm-publish-summary.json` envelope `{ publishedPackages: [...] }` is preserved, but each entry is upgraded to the same per-package shape (additive — `name` and `version` are still present).
- `pnpm config get @<scope>:registry` now reports the same URL that `pnpm publish` and the resolvers actually use. Previously, `config get` only consulted `.npmrc`, while `publish`/install used the merged map that includes `pnpm-workspace.yaml`'s `registries` block — so the two could diverge silently and a publish could go to the wrong registry [#​11492](https://github.com/pnpm/pnpm/issues/11492).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.6`](https://github.com/pnpm/pnpm/releases/tag/v11.0.6): pnpm 11.0.6
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.5...v11.0.6)
#### Patch Changes
- Fix `pnpm_config_npmrc_auth_file` and `pnpm_config_userconfig` env vars not actually loading the custom `.npmrc`. The env vars were parsed and assigned to the resolved config, but only after `loadNpmrcConfig` had already read the default `~/.npmrc` — so the custom file path was set but never read. The relevant env vars are now consulted before the user-level `.npmrc` is loaded [#​11465](https://github.com/pnpm/pnpm/issues/11465).
- Preserve the original key order in `pnpm-workspace.yaml` when updating it. Existing keys keep their position, and new keys are inserted in alphabetical position when the existing keys are already sorted (with a leading `packages` key allowed) or appended at the end otherwise.
- Fixed `pnpm self-update` on installations originally set up by pnpm v10. v10 added `PNPM_HOME` directly to PATH and wrote a `pnpm` bootstrap shim there. v11 setup writes shims under `PNPM_HOME/bin` instead, so when a v10 user upgrades to v11 the legacy shim at `PNPM_HOME` keeps pointing into the old `.tools/<version>` install — `pnpm --version` continues to report the pre-update version even though the new version was installed under `global/v11`. Self-update now detects this layout, refreshes the legacy shims so the upgrade actually takes effect, and prints a hint suggesting `pnpm setup` to migrate PATH to the v11 layout. [#​11464](https://github.com/pnpm/pnpm/issues/11464).
- Print a warning when settings that are not allowed in the global config file (e.g. `nodeLinker`, `hoistPattern`) are present in `config.yaml` and silently ignored. Previously these settings were dropped without any feedback, leaving users unsure why their global configuration had no effect. The warning suggests moving those settings to a project-level `pnpm-workspace.yaml`, or sharing them across projects via [config dependencies](https://pnpm.io/11.x/config-dependencies).
- Throw a pnpm error when `overrides` has an invalid shape or contains a non-string value.
- Validate all `readPackage` dependency map fields, including `devDependencies`, and reject falsy non-object invalid values instead of silently accepting them.
- Prevent crashes during `pnpm config`, `pnpm set`, and `pnpm get` by tolerating `configDependencies` install failures. For these commands, a failure to install `configDependencies` (for example because the registry auth token has not been written yet) is now logged at debug level and the command proceeds. All other commands still surface the install error [#​10684](https://github.com/pnpm/pnpm/issues/10684).
- Treat `allowBuilds` as an install-state input and clear previously ignored builds when they are explicitly disallowed.
- Fixes [#​10594](https://github.com/pnpm/pnpm/issues/10594), catalogs not being read from the workspace when using the `catalog:` protocol with the `pnpm dlx` / `pnpx` command, resulting in a catalog entry not found error.
- Accept `PNPM_CONFIG_*` (uppercase) environment variables in addition to `pnpm_config_*`. Previously, only the lowercase form was honored, so env vars renamed per the v11 migration guide (e.g. `PNPM_CONFIG_USERCONFIG`) silently had no effect on case-sensitive systems like macOS and Linux [#​11465](https://github.com/pnpm/pnpm/issues/11465).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.5`](https://github.com/pnpm/pnpm/releases/tag/v11.0.5): pnpm 11.0.5
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.4...v11.0.5)
#### Patch Changes
- Drop the `darwin-x64` artifact from `@pnpm/exe` and from the GitHub release page. The Node.js SEA mechanism `pnpm pack-app` uses produces a binary that segfaults at startup on Intel Macs because of an upstream Node.js bug ([nodejs/node#62893](https://github.com/nodejs/node/issues/62893), tracked alongside [#​59553](https://github.com/nodejs/node/issues/59553); the Node.js team has [opted not to fix it](https://github.com/nodejs/node/pull/60250) on the grounds that x64 macOS is being phased out). Re-signing with `codesign` or `ldid` doesn't help — the corruption is in LIEF's Mach-O surgery, before signing.
Intel Mac users should install pnpm via `npm install -g pnpm` (uses the system Node.js, no SEA), or stay on pnpm 10.x. `@pnpm/exe`'s preinstall on Intel Mac now exits with a clear error pointing at these alternatives.
Closes [#​11423](https://github.com/pnpm/pnpm/issues/11423).
- `pnpm dlx` (and `pnpx`/`pnx`/`pnpm create`) now runs the same interactive `approve-builds` prompt as `pnpm add -g` when the package being launched depends on transitive packages with install scripts. Previously, the v11 `strictDepBuilds` default made dlx fail with `ERR_PNPM_IGNORED_BUILDS` and required users to re-run with `--allow-build=<pkg>` for every offending dependency. dlx also now removes the partially-populated cache directory when the install fails, so a subsequent run starts clean instead of reusing a broken install whose builds were silently skipped [#​11444](https://github.com/pnpm/pnpm/issues/11444).
- [`72629fc`](https://github.com/pnpm/pnpm/commit/72629fc): Fix `pnpm -g ls --json` and `pnpm -g ls --parseable` so they emit valid JSON and parseable output respectively, matching pnpm 10 behavior. Since the isolated global packages refactor in pnpm 11, the global list command had a custom path that always printed plain text and ignored `--json`/`--parseable`, which broke tools like `npm-check-updates` that parse the JSON output [#​11440](https://github.com/pnpm/pnpm/issues/11440).
`pnpm -g ls --depth=<n>` (with n > 0) now errors when more than one isolated global install would be involved, since each install has its own lockfile and merging their transitive trees would be incoherent. When the request can be narrowed to a single install group, the regular `list` flow is used and the full dependency tree is shown.
- Fixed `pnpm publish` to honor `publishConfig.registry` from `package.json` when publishing a single package. The native publish flow introduced in v11 was reading the registry from `.npmrc` only, ignoring the per-package override [#​11419](https://github.com/pnpm/pnpm/issues/11419).
- When `strictPeerDependencies` is `true`, the `ERR_PNPM_PEER_DEP_ISSUES` error once again renders the peer dependency issues inline using the same format as `pnpm peers check`, so users (and CI tools like Renovate) can see what failed without running `pnpm peers check` separately [#​11439](https://github.com/pnpm/pnpm/issues/11439).
- The `WARN` and error code labels in pnpm's output now wrap in brackets (`[WARN]`, `[ERR_PNPM_FOO]`). Previously the labels relied entirely on a colored background to stand out, which meant they blended into the surrounding text in terminals without color (e.g. when `NO_COLOR` is set or output is piped). The brackets are painted in the same color as the badge background, so they appear as ordinary padding in color-capable terminals — only the no-color rendering changes.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.4`](https://github.com/pnpm/pnpm/releases/tag/v11.0.4): pnpm 11.0.4
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.3...v11.0.4)
#### Patch Changes
- Fixed `pnpm ci` not reinstalling workspace package `node_modules` directories after the clean step [#​11427](https://github.com/pnpm/pnpm/issues/11427).
- Remove pnpm's workspace state file when cleaning node\_modules so `pnpm ci` performs a fresh install after the clean step.
- Do not remove `pnpm-lock.yaml` during `pnpm clean` when `lockfile: true` is configured in `pnpm-workspace.yaml`. The lockfile is only removed when the `--lockfile` option is passed to `pnpm clean`.
- `pnpm self-update` (with no version argument) no longer downgrades pnpm when the registry's `latest` dist-tag points to an older release than the currently active version. Run `pnpm self-update latest` to force a downgrade [#​11418](https://github.com/pnpm/pnpm/issues/11418).
- `minimumReleaseAgeStrict` now defaults to `true` whenever the user explicitly sets `minimumReleaseAge` (via `pnpm-workspace.yaml`, the global `config.yaml`, the CLI, or `pnpm_config_*` env vars).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.3`](https://github.com/pnpm/pnpm/releases/tag/v11.0.3): pnpm 11.0.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.2...v11.0.3)
#### Patch Changes
- Fix too many open files error sometimes happening on Windows, when creating command shims in `node_modules/.bin` [#​11412](https://github.com/pnpm/pnpm/issues/11412).
- Fix `ERR_PNPM_FETCH_404` when installing a project whose lockfile depends on a `file:` tarball. The previous behavior dropped the `tarball` field from `file:` and git-hosted resolutions when `lockfile-include-tarball-url=false` (the default), even though those URLs cannot be reconstructed from the package name, version, and registry [#​11407](https://github.com/pnpm/pnpm/issues/11407).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.2`](https://github.com/pnpm/pnpm/releases/tag/v11.0.2): pnpm 11.0.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.1...v11.0.2)
#### Patch Changes
- Fix `ENOENT` symlink failure when `pnpm add -g` triggers the approve-builds prompt. The global add flow used to forward an absolute `modulesDir` (`<installDir>/node_modules`) into the install run by `approve-builds`. The install layer treated `modulesDir` as a path relative to `lockfileDir` and joined it again, producing a doubled path on Windows because `path.join` does not collapse an embedded absolute path. The hoist step then tried to `mkdir` and symlink under `<installDir>\<installDir>\node_modules\.pnpm\node_modules\...` and failed with `ENOENT` [#​11403](https://github.com/pnpm/pnpm/issues/11403).
- Fixed `packageManagerDependencies` going stale when pnpm is invoked through corepack. The lockfile sync (and the `devEngines.packageManager` version check) previously ran only when pnpm was invoked directly; under corepack the entire block was skipped, so a stale entry would persist even after the running pnpm version changed. The lockfile sync now runs regardless of how pnpm was invoked, while the pnpm-managed version switch (`onFail: 'download'`) remains skipped under corepack so it doesn't fight corepack's own version selection [#​11397](https://github.com/pnpm/pnpm/issues/11397).
- Fix recursive publish summaries to report the manifest from `publishConfig.directory` when packages publish from a generated directory [#​11239](https://github.com/pnpm/pnpm/issues/11239).
- Fix negated `os` / `cpu` entries (e.g. `["!win32"]`) being incorrectly rejected when `supportedArchitectures` expands to multiple platforms [#​11375](https://github.com/pnpm/pnpm/pull/11375).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.1`](https://github.com/pnpm/pnpm/releases/tag/v11.0.1): pnpm 11.0.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v11.0.0...v11.0.1)
#### Patch Changes
- Report unknown top-level options before falling back to implicit `pnpm run` scripts.
- Reject `null` named catalogs in workspace manifests with `InvalidWorkspaceManifestError` instead of crashing with a raw `TypeError`.
- Populate download location for git-sourced dependencies in SBOM output. Previously `pnpm sbom` emitted `NOASSERTION` (SPDX) and omitted the distribution reference (CycloneDX) for git dependencies. Now emits the git URL with commit hash, e.g. `git+https://github.com/user/repo.git#commit`.
- `pnpm self-update` now keeps `package.json`'s `packageManager` and `devEngines.packageManager` in sync. When the legacy `packageManager` field pins pnpm, both fields are rewritten to the new exact pnpm version on update — `packageManager` to `pnpm@<version>` (without an integrity hash), and `devEngines.packageManager.version` to the same exact `<version>` (dropping any range operator). When only `devEngines.packageManager` is declared, the existing range-preserving behavior is unchanged [#​11388](https://github.com/pnpm/pnpm/issues/11388).
- Sort the keys of the overrides object returned by `pnpm audit --fix` so that the log output order matches the order written to `pnpm-workspace.yaml`.
- Update the env lockfile's `packageManagerDependencies` entry when `devEngines.packageManager` declares a pnpm version that the lockfile no longer satisfies. Previously, the stale entry was kept even though the running pnpm matched the declared version, silently breaking the integrity record [#​11387](https://github.com/pnpm/pnpm/issues/11387).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v11.0.0`](https://github.com/pnpm/pnpm/releases/tag/v11.0.0): pnpm 11
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.5...v11.0.0)
> Migration guide: [Migrating from v10 to v11](https://pnpm.io/11.x/migration)
#### Highlights
##### Major
- **Node.js 22+ required** — support for Node 18, 19, 20, and 21 is dropped, pnpm itself is now pure ESM, and the standalone exe requires glibc 2.27.
- **Supply-chain protection on by default** — `minimumReleaseAge` defaults to 1 day (newly published packages are not resolved for 24h) and `blockExoticSubdeps` defaults to `true`.
- **`allowBuilds` replaces the old build-dependency settings** — `onlyBuiltDependencies`, `onlyBuiltDependenciesFile`, `neverBuiltDependencies`, `ignoredBuiltDependencies`, and `ignoreDepScripts` have been removed.
- **Global installs are isolated and use the global virtual store by default** — each `pnpm add -g` gets its own directory with its own `package.json`, `node_modules`, and lockfile.
- **New SQLite-backed store index** (store v11) with bundled manifests and hex digests, reducing filesystem syscalls and speeding up installation.
- **Native publish flow** — [`pnpm publish`](https://pnpm.io/11.x/cli/publish), [`login`](https://pnpm.io/11.x/cli/login), [`logout`](https://pnpm.io/11.x/cli/logout), [`view`](https://pnpm.io/11.x/cli/view), [`deprecate`](https://pnpm.io/11.x/cli/deprecate), [`unpublish`](https://pnpm.io/11.x/cli/unpublish), [`dist-tag`](https://pnpm.io/11.x/cli/dist-tag), and [`version`](https://pnpm.io/11.x/cli/version) no longer delegate to the npm CLI, and the remaining npm passthrough commands now throw "not implemented".
- **[`pnpm audit`](https://pnpm.io/11.x/cli/audit) uses npm's bulk advisories endpoint** — the legacy `/security/audits` endpoints are gone. CVE-based filtering has been replaced with GHSA-based filtering: migrate `auditConfig.ignoreCves` entries to `auditConfig.ignoreGhsas`.
- **`.npmrc` is auth/registry only** — all other settings must live in `pnpm-workspace.yaml` or the new global `config.yaml`, and environment variables use the `pnpm_config_*` prefix.
- **Runtime installs are slimmer** — installing a Node.js runtime via `node@runtime:<version>` no longer extracts the bundled `npm`, `npx`, and `corepack`, roughly halving the files pnpm has to hash, write, and link.
##### Minor
- **New commands:** [`pnpm ci`](https://pnpm.io/11.x/cli/ci), [`pnpm sbom`](https://pnpm.io/11.x/cli/sbom), [`pnpm clean`](https://pnpm.io/11.x/cli/clean), [`pnpm peers check`](https://pnpm.io/11.x/cli/peers), [`pnpm runtime set`](https://pnpm.io/11.x/cli/runtime), [`pnpm docs`](https://pnpm.io/11.x/cli/docs)/`home`, [`pnpm ping`](https://pnpm.io/11.x/cli/ping), [`pnpm search`](https://pnpm.io/11.x/cli/search), [`pnpm star`](https://pnpm.io/11.x/cli/star)/`unstar`/`stars`, [`pnpm whoami`](https://pnpm.io/11.x/cli/whoami), [`pnpm with`](https://pnpm.io/11.x/cli/with), and [`pnpm pack-app`](https://pnpm.io/11.x/cli/pack-app), plus `pn`/[`pnx`](https://pnpm.io/11.x/cli/pnx) short aliases.
- **ESM pnpmfiles** via `.pnpmfile.mjs`, which takes priority over `.pnpmfile.cjs` when present.
- **[`pnpm audit --fix=update`](https://pnpm.io/11.x/cli/audit)** fixes vulnerabilities by updating packages in the lockfile instead of adding overrides, and `pnpm audit --fix --interactive` lets you select which advisories to fix.
- **[`pnpm pack-app`](https://pnpm.io/11.x/cli/pack-app)** packs a CommonJS entry into a standalone executable for one or more target platforms using Node.js Single Executable Applications.
- **Faster HTTP and I/O** — undici with Happy Eyeballs, direct-to-CAS writes, skipped staging directory, pre-allocated tarball downloads, and an NDJSON metadata cache.
#### Major Changes
##### Requirements
- pnpm is now distributed as pure ESM.
- Dropped support for Node.js v18, 19, 20, and 21.
- The standalone exe version of pnpm requires at least glibc 2.27.
##### Security & Build Defaults
- Changed default values: `optimisticRepeatInstall` is now `true`, `verifyDepsBeforeRun` is now `install`, `minimumReleaseAge` is now `1440` (1 day), and `minimumReleaseAgeStrict` is `false`. Newly published packages will not be resolved until they are at least 1 day old. This protects against supply chain attacks by giving the community time to detect and remove compromised versions. To opt out, set `minimumReleaseAge: 0` in `pnpm-workspace.yaml` [#​11158](https://github.com/pnpm/pnpm/pull/11158).
- `strictDepBuilds` is `true` by default.
- `blockExoticSubdeps` is `true` by default.
- Removed deprecated build dependency settings: `onlyBuiltDependencies`, `onlyBuiltDependenciesFile`, `neverBuiltDependencies`, `ignoredBuiltDependencies`, and `ignoreDepScripts` [#​11220](https://github.com/pnpm/pnpm/pull/11220).
Use the `allowBuilds` setting instead. It is a map where keys are package name patterns and values are booleans:
- `true` means the package is allowed to run build scripts
- `false` means the package is explicitly denied from running build scripts
Same as before, by default, none of the packages in the dependencies are allowed to run scripts. If a package has postinstall scripts and it isn't declared in `allowBuilds`, an error is printed.
Before:
```yaml
onlyBuiltDependencies:
- electron
onlyBuiltDependenciesFile: "allowed-builds.json"
neverBuiltDependencies:
- core-js
ignoredBuiltDependencies:
- esbuild
```
After:
```yaml
allowBuilds:
electron: true
core-js: false
esbuild: false
```
- Removed `allowNonAppliedPatches` in favor of `allowUnusedPatches`.
- Removed `ignorePatchFailures`; patch application failures now throw an error.
##### Store
- Runtime dependencies are always linked from the global virtual store [#​10233](https://github.com/pnpm/pnpm/pull/10233).
- Optimized index file format to store the hash algorithm once per file instead of repeating it for every file entry. Each file entry now stores only the hex digest instead of the full integrity string (`<algo>-<digest>`). Using hex format improves performance since file paths in the content-addressable store use hex representation, eliminating base64-to-hex conversion during path lookups.
- Store version bumped to v11.
- The bundled manifest (name, version, bin, engines, scripts, etc.) is now stored directly in the package index file, eliminating the need to read `package.json` from the content-addressable store during resolution and installation. This reduces I/O and speeds up repeat installs [#​10473](https://github.com/pnpm/pnpm/pull/10473).
- The package index in the content-addressable store is now backed by SQLite. Instead of individual JSON files under `$STORE/index/`, package metadata is stored in a single SQLite database at `$STORE/index.db` with MessagePack-encoded values. This reduces filesystem syscall overhead, improves space efficiency for small metadata entries, and enables concurrent access via SQLite's WAL mode. Packages missing from the new index are re-fetched on demand [#​10500](https://github.com/pnpm/pnpm/pull/10500) [#​10826](https://github.com/pnpm/pnpm/issues/10826).
##### Global Packages
- Global installs (`pnpm add -g pkg`) and `pnx` now use the global virtual store by default. Packages are stored at `{storeDir}/links` instead of per-project `.pnpm` directories. This can be disabled by setting `enableGlobalVirtualStore: false` [#​10694](https://github.com/pnpm/pnpm/pull/10694).
- Isolated global packages. Each globally installed package (or group of packages installed together) now gets its own isolated installation directory with its own `package.json`, `node_modules/`, and lockfile. This prevents global packages from interfering with each other through peer dependency conflicts, hoisting changes, or version resolution shifts.
Key changes:
- `pnpm add -g <pkg>` creates an isolated installation in `{pnpmHomeDir}/global/v11/{hash}/`
- `pnpm remove -g <pkg>` removes the entire installation group containing the package
- `pnpm update -g [pkg]` re-installs packages in new isolated directories
- `pnpm list -g` scans isolated directories to show all installed global packages
- `pnpm install -g` (no args) is no longer supported; use `pnpm add -g <pkg>` instead
- Globally installed binaries are now stored in a `bin` subdirectory of `PNPM_HOME` instead of directly in `PNPM_HOME`. This prevents internal directories like `global/` and `store/` from polluting shell autocompletion when `PNPM_HOME` is on PATH [#​10986](https://github.com/pnpm/pnpm/issues/10986). After upgrading, run `pnpm setup` to update your shell configuration.
- Breaking changes to `pnpm link`:
- `pnpm link <pkg-name>` no longer resolves packages from the global store. Only relative or absolute paths are accepted. For example, use `pnpm link ./foo` instead of `pnpm link foo`.
- `pnpm link --global` is removed. Use `pnpm add -g .` to register a local package's bins globally.
- `pnpm link` (no arguments) is removed. Use `pnpm link <dir>` with an explicit path instead.
##### Configuration
- pnpm no longer reads all settings from `.npmrc`. Only auth and registry settings are read from `.npmrc` files. All other settings (like `hoistPattern`, `nodeLinker`, `shamefullyHoist`, etc.) must be configured in `pnpm-workspace.yaml` or the global `~/.config/pnpm/config.yaml` [#​11189](https://github.com/pnpm/pnpm/pull/11189).
- Network settings (`httpProxy`, `httpsProxy`, `noProxy`, `localAddress`, `strictSsl`, `gitShallowHosts`) are now written to `config.yaml` (global) or `pnpm-workspace.yaml` (local) instead of `.npmrc`/`auth.ini`. They are still readable from `.npmrc` for easier migration from the npm CLI [#​11209](https://github.com/pnpm/pnpm/pull/11209).
pnpm no longer reads `npm_config_*` environment variables. Use `pnpm_config_*` environment variables instead (e.g., `pnpm_config_registry` instead of `npm_config_registry`).
pnpm no longer reads the npm global config at `$PREFIX/etc/npmrc`.
`pnpm login` writes auth tokens to `~/.config/pnpm/auth.ini`.
New `registries` setting in `pnpm-workspace.yaml`:
```yaml
registries:
default: https://registry.npmjs.org/
"@my-org": https://private.example.com/
"@internal": https://nexus.corp.com/
```
Auth tokens in `~/.npmrc` still work — pnpm continues to read `~/.npmrc` as a fallback for registry authentication. The new `npmrcAuthFile` setting can be used to point to a different file instead of `~/.npmrc`.
- Replace workspace project specific `.npmrc` with `packageConfigs` in `pnpm-workspace.yaml`.
A workspace manifest with `packageConfigs` looks something like this:
```yaml
# File: pnpm-workspace.yaml
packages:
- "packages/project-1"
- "packages/project-2"
packageConfigs:
"project-1":
saveExact: true
"project-2":
savePrefix: "~"
```
Or this:
```yaml
# File: pnpm-workspace.yaml
packages:
- "packages/project-1"
- "packages/project-2"
packageConfigs:
- match: ["project-1", "project-2"]
modulesDir: "node_modules"
saveExact: true
```
- pnpm no longer reads settings from the `pnpm` field of `package.json`. Settings should be defined in `pnpm-workspace.yaml` [#​10086](https://github.com/pnpm/pnpm/pull/10086).
- `pnpm config get` (without `--json`) no longer prints INI formatted text. Instead, it prints JSON for objects and arrays, and raw strings for strings, numbers, booleans, and nulls. `pnpm config get --json` still prints all types of values as JSON, as before.
- `pnpm config get <array>` now prints a JSON array.
- `pnpm config list` now prints a JSON object instead of INI formatted text.
- `pnpm config list` and `pnpm config get` (without argument) now hide auth-related settings.
- `pnpm config list` and `pnpm config get` (without argument) now show top-level keys as camelCase. Exception: keys that start with `@` or `//` are preserved (their cases don't change).
- `pnpm config get` and `pnpm config list` no longer load non-camelCase options from the workspace manifest (`pnpm-workspace.yaml`).
##### Removed Commands & npm Passthrough
- pnpm no longer falls back to the npm CLI. Commands that were previously passed through to npm (`access`, `bugs`, `docs`, `edit`, `find`, `home`, `issues`, `owner`, `ping`, `prefix`, `profile`, `pkg`, `repo`, `search`, `set-script`, `star`, `stars`, `team`, `token`, `unstar`, `whoami`, `xmas`) and their aliases (`s`, `se`) now throw a "not implemented" error, with a suggestion to use the npm CLI directly [#​10642](https://github.com/pnpm/pnpm/pull/10642). Other previously passed-through commands — [`view`](https://pnpm.io/11.x/cli/view) (`info`, `show`, `v`), [`login`](https://pnpm.io/11.x/cli/login) (`adduser`), [`logout`](https://pnpm.io/11.x/cli/logout), [`deprecate`](https://pnpm.io/11.x/cli/deprecate), [`unpublish`](https://pnpm.io/11.x/cli/unpublish), [`dist-tag`](https://pnpm.io/11.x/cli/dist-tag), and [`version`](https://pnpm.io/11.x/cli/version) — have been reimplemented natively in pnpm (see New Commands below).
- [`pnpm publish`](https://pnpm.io/11.x/cli/publish) now works without the `npm` CLI.
The One-time Password feature now reads from `PNPM_CONFIG_OTP` instead of `NPM_CONFIG_OTP`:
```sh
export PNPM_CONFIG_OTP='<your OTP here>'
pnpm publish --no-git-checks
```
If the registry requests OTP and the user has not provided it via the `PNPM_CONFIG_OTP` environment variable or the `--otp` flag, pnpm will prompt the user directly for an OTP code.
If the registry requests web-based authentication, pnpm will print a scannable QR code along with the URL.
Since the new `pnpm publish` no longer calls `npm publish`, some undocumented features may have been unknowingly dropped. If you rely on a feature that is now gone, please open an issue at <https://github.com/pnpm/pnpm/issues>. In the meantime, you can use `pnpm pack && npm publish *.tgz` as a workaround.
- Removed the `pnpm server` command [#​10463](https://github.com/pnpm/pnpm/pull/10463).
- Removed support for the `useNodeVersion` and `executionEnv.nodeVersion` fields. `devEngines.runtime` and `engines.runtime` should be used instead [#​10373](https://github.com/pnpm/pnpm/pull/10373).
- Removed support for `hooks.fetchers`. We now have a new API for custom fetchers and resolvers via the `fetchers` field of `pnpmfile`.
##### Lifecycle Scripts
- pnpm no longer populates `npm_config_*` environment variables from the pnpm config during lifecycle scripts. Only well-known `npm_*` env vars are now set, matching Yarn's behavior [#​11116](https://github.com/pnpm/pnpm/pull/11116).
##### CLI Output
- Cleaner output for script execution: pnpm now prints `$ command` instead of `> pkg@version stage path\n> command`, and shows project name and path only when running in a different directory. The `$ command` line is printed to stderr to keep stdout clean for piping [#​11132](https://github.com/pnpm/pnpm/pull/11132).
- During install, instead of rendering the full peer dependency issues tree, pnpm now suggests running [`pnpm peers check`](https://pnpm.io/11.x/cli/peers) to view the issues [#​11133](https://github.com/pnpm/pnpm/pull/11133).
##### Lockfile
- Simplified `patchedDependencies` lockfile format from `Record<string, { path: string, hash: string }>` to `Record<string, string>` (selector to hash). Existing lockfiles with the old format are automatically migrated [#​10911](https://github.com/pnpm/pnpm/pull/10911).
##### Other
- The default value of the `type` field in the `package.json` file of the project initialized by `pnpm init` command has been changed to `module`.
- Added support for lowercase options in `pnpm add`: `-d`, `-p`, `-o`, `-e` [#​9197](https://github.com/pnpm/pnpm/issues/9197).
When using the `pnpm add` command only:
- `-p` is now an alias for `--save-prod` instead of `--parseable`
- `-d` is now an alias for `--save-dev` instead of `--loglevel=info`
- The root workspace project is no longer excluded when it is explicitly selected via a filter [#​10465](https://github.com/pnpm/pnpm/pull/10465).
##### Audit
- [`pnpm audit`](https://pnpm.io/11.x/cli/audit) now calls npm's `/-/npm/v1/security/advisories/bulk` endpoint. The legacy `/-/npm/v1/security/audits{,/quick}` endpoints have been retired by the registry, so the legacy request/response contract is no longer supported.
The bulk endpoint does not return CVE identifiers. CVE-based filtering has been replaced with GitHub advisory ID (GHSA) filtering:
- `auditConfig.ignoreCves` → `auditConfig.ignoreGhsas` (the previous key is no longer recognized)
- `pnpm audit --ignore <id>` / `pnpm audit --ignore-unfixable` now read and write GHSAs instead of CVEs
- GHSAs are derived from each advisory's `url` (`https://github.com/advisories/GHSA-xxxx-xxxx-xxxx`)
To migrate: replace each `CVE-YYYY-NNNNN` entry in your `auditConfig.ignoreCves` with the corresponding `GHSA-xxxx-xxxx-xxxx` value (visible in the `More info` column of `pnpm audit` output) and move it under `auditConfig.ignoreGhsas`.
##### Package Manager Settings
- **Breaking:** removed the `managePackageManagerVersions`, `packageManagerStrict`, and `packageManagerStrictVersion` settings. They existed only to derive the `onFail` behavior for the legacy `packageManager` field, and the `pmOnFail` setting introduced alongside [`pnpm with`](https://pnpm.io/11.x/cli/with) subsumes all three — it directly sets the `onFail` behavior of both `packageManager` and `devEngines.packageManager`. The `COREPACK_ENABLE_STRICT` environment variable is no longer honored (it only gated `packageManagerStrict`); use `pmOnFail` instead.
Migration:
| Removed setting | Replace with |
| ------------------------------------- | ------------------------------ |
| `managePackageManagerVersions: true` | `pmOnFail: download` (default) |
| `managePackageManagerVersions: false` | `pmOnFail: ignore` |
| `packageManagerStrict: false` | `pmOnFail: warn` |
| `packageManagerStrictVersion: true` | `pmOnFail: error` |
| `COREPACK_ENABLE_STRICT=0` | `pmOnFail: warn` |
##### Runtime Installs
- Installing a Node.js runtime via `node@runtime:<version>` (including `pnpm env use` and `pnpm runtime set node`) no longer extracts the bundled `npm`, `npx`, and `corepack` from the Node.js archive. This cuts roughly half of the files pnpm has to hash, write to the CAS, and link during installation, making runtime installs noticeably faster. Users who still need `npm` can install it as a separate package.
#### Minor Changes
##### New Commands
- Added native [`pnpm view`](https://pnpm.io/11.x/cli/view) (`info`, `show`, `v`) command for viewing package metadata from the registry [#​11064](https://github.com/pnpm/pnpm/pull/11064).
- Added [`pnpm login`](https://pnpm.io/11.x/cli/login) (and `pnpm adduser` alias) command for authenticating with npm registries. Supports web-based login with QR code as well as classic username/password login [#​11094](https://github.com/pnpm/pnpm/pull/11094).
- Added [`pnpm logout`](https://pnpm.io/11.x/cli/logout) command for logging out of npm registries. Revokes the authentication token on the registry and removes it from the local auth config file [#​11213](https://github.com/pnpm/pnpm/pull/11213).
- Added native [`pnpm deprecate`](https://pnpm.io/11.x/cli/deprecate) and `pnpm undeprecate` commands for setting and removing deprecation messages on package versions without delegating to the npm CLI [#​11120](https://github.com/pnpm/pnpm/pull/11120).
- Added native [`pnpm unpublish`](https://pnpm.io/11.x/cli/unpublish) command. Supports unpublishing specific versions, version ranges via semver, and entire packages with `--force` [#​11128](https://github.com/pnpm/pnpm/pull/11128).
- Added native [`pnpm dist-tag`](https://pnpm.io/11.x/cli/dist-tag) command (`ls`, `add`, `rm` subcommands) [#​11218](https://github.com/pnpm/pnpm/pull/11218).
- Added [`pnpm sbom`](https://pnpm.io/11.x/cli/sbom) command for generating Software Bill of Materials in CycloneDX 1.7 and SPDX 2.3 JSON formats [#​9088](https://github.com/pnpm/pnpm/issues/9088).
- Added [`pnpm clean`](https://pnpm.io/11.x/cli/clean) command that safely removes `node_modules` directories from all workspace projects [#​10707](https://github.com/pnpm/pnpm/issues/10707). Use `--lockfile` to also remove `pnpm-lock.yaml` files.
- Added a new command [`pnpm runtime set <runtime name> <runtime version spec> [-g]`](https://pnpm.io/11.x/cli/runtime) for installing runtimes. Deprecated `pnpm env use` in favor of the new command.
- Added the ability to fix vulnerabilities by updating packages in the lockfile instead of adding overrides. Use [`pnpm audit --fix=update`](https://pnpm.io/11.x/cli/audit) [#​10341](https://github.com/pnpm/pnpm/pull/10341).
- Added [`pnpm ci`](https://pnpm.io/11.x/cli/ci) command for clean installs [#​6100](https://github.com/pnpm/pnpm/issues/6100). The command runs `pnpm clean` followed by `pnpm install --frozen-lockfile`. Designed for CI/CD environments where reproducible builds are critical. Aliases: `pnpm clean-install`, `pnpm ic`, `pnpm install-clean` [#​11003](https://github.com/pnpm/pnpm/pull/11003).
- Added [`pnpm peers check`](https://pnpm.io/11.x/cli/peers) command that checks for unmet and missing peer dependency issues by reading the lockfile [#​7087](https://github.com/pnpm/pnpm/issues/7087).
- Implemented the [`version`](https://pnpm.io/11.x/cli/version) command natively in pnpm to support workspaces and `workspace:` protocols correctly. The new command allows bumping package versions (major, minor, patch, etc.) with full workspace support and git integration [#​10879](https://github.com/pnpm/pnpm/pull/10879).
- [`pnpm audit --fix`](https://pnpm.io/11.x/cli/audit) now supports a new interactive mode via `--interactive`/`-i`.
- Added the [`pnpm docs`](https://pnpm.io/11.x/cli/docs) command and its alias `pnpm home`. This command opens the package documentation or homepage in the browser. When the package has no valid homepage, it falls back to `https://npmx.dev/package/<name>`.
- Added native [`pnpm ping`](https://pnpm.io/11.x/cli/ping) command to test registry connectivity. Provides a simple way to verify connectivity to the configured registry without requiring external tools.
- Implemented native [`search`](https://pnpm.io/11.x/cli/search) command and its aliases (`s`, `se`, `find`).
- Implemented native [`star`, `unstar`, `stars`](https://pnpm.io/11.x/cli/star), and [`whoami`](https://pnpm.io/11.x/cli/whoami) commands.
- Add [`pnpm with <version|current> <args...>`](https://pnpm.io/11.x/cli/with) command. Runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project's `packageManager` and `devEngines.packageManager` pins.
- Added a new [`pnpm pack-app`](https://pnpm.io/11.x/cli/pack-app) command that packs a CommonJS entry file into a standalone executable for one or more target platforms, using the [Node.js Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) API under the hood.
##### Configuration
- Added support for a global YAML config file named `config.yaml`.
Configuration is now split into two categories:
- Registry and auth settings, which can be stored in INI files such as the global `rc` file and local `.npmrc`.
- pnpm-specific settings, which can only be loaded from YAML files such as the global `config.yaml` and local `pnpm-workspace.yaml`.
- Added support for loading environment variables whose names start with `pnpm_config_` into config. These environment variables override settings from `pnpm-workspace.yaml` but not CLI arguments.
- Added support for reading `allowBuilds` from `pnpm-workspace.yaml` in the global package directory for global installs.
- Added support for `pnpm config get globalconfig` to retrieve the global config file path [#​9977](https://github.com/pnpm/pnpm/issues/9977).
- Added a new setting `virtualStoreOnly` that populates the virtual store without creating importer symlinks, hoisting, bin links, or running lifecycle scripts. This is useful for pre-populating a store (e.g., in Nix builds) without creating unnecessary project-level artifacts. `pnpm fetch` now uses this mode internally [#​10840](https://github.com/pnpm/pnpm/issues/10840).
- Added support for specifying the pnpm version via `devEngines.packageManager` in `package.json`. Unlike the `packageManager` field, this supports version ranges. The resolved version is stored in `pnpm-lock.yaml` and reused if it still satisfies the range [#​10932](https://github.com/pnpm/pnpm/pull/10932).
- Added a new `dedupePeers` setting that reduces peer dependency duplication. When enabled, peer dependency suffixes use version-only identifiers (`name@version`) instead of full dep paths, eliminating nested suffixes like `(foo@1.0.0(bar@2.0.0))`. This dramatically reduces the number of package instances in projects with many recursive peer dependencies [#​11070](https://github.com/pnpm/pnpm/issues/11070).
- Config dependencies are now installed into the global virtual store (`{storeDir}/links/`) and symlinked into `node_modules/.pnpm-config/`. This allows config dependencies to be shared across projects that use the same store, avoiding redundant fetches and imports [#​10910](https://github.com/pnpm/pnpm/pull/10910). Config dependency and package manager integrity info is now stored in `pnpm-lock.yaml` instead of inlined in `pnpm-workspace.yaml`: the workspace manifest contains only clean version specifiers for `configDependencies`, while the resolved versions, integrity hashes, and tarball URLs are recorded in the lockfile as a separate YAML document. The env lockfile section also stores `packageManagerDependencies` resolved during version switching and self-update. Projects using the old inline-hash format are automatically migrated on install [#​10912](https://github.com/pnpm/pnpm/pull/10912) [#​10964](https://github.com/pnpm/pnpm/pull/10964).
- Added `nodeDownloadMirrors` setting to configure custom Node.js download mirrors in `pnpm-workspace.yaml`. This replaces the `node-mirror:<channel>` `.npmrc` setting, which is no longer read [#​11194](https://github.com/pnpm/pnpm/pull/11194):
```yaml
nodeDownloadMirrors:
release: https://my-mirror.example.com/download/release/
```
- `pnpm dlx` and `pnpm create` now respect security and trust policy settings (`minimumReleaseAge`, `minimumReleaseAgeExclude`, `minimumReleaseAgeStrict`, `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`) from project-level configuration [#​11183](https://github.com/pnpm/pnpm/issues/11183).
- `pnpm init` now writes a `devEngines.packageManager` field instead of the `packageManager` field when `init-package-manager` is enabled.
- Added a new setting `runtimeOnFail` that overrides the `onFail` field of `devEngines.runtime` (and `engines.runtime`) in the root project's `package.json`. Accepted values: `ignore`, `warn`, `error`, `download`. For example, setting `runtimeOnFail=download` makes pnpm download the declared runtime version even when the manifest does not set `onFail: "download"`.
- Added a new setting `minimumReleaseAgeIgnoreMissingTime`, which is `true` by default. When enabled, pnpm skips the `minimumReleaseAge` maturity check if the registry metadata does not include the `time` field. Set to `false` to fail resolution instead.
##### Store
- When the global virtual store is enabled, packages that are not allowed to build (and don't transitively depend on packages that are) now get hashes that don't include the engine name (platform, architecture, Node.js major version). This means \~95% of packages in the GVS survive Node.js upgrades and architecture changes without re-import [#​10837](https://github.com/pnpm/pnpm/issues/10837).
##### Hooks & Pnpmfiles
- Added support for pnpmfiles written in ESM, using the `.mjs` extension. When `.pnpmfile.mjs` exists, it takes priority over `.pnpmfile.cjs` and only one is loaded [#​9730](https://github.com/pnpm/pnpm/pull/9730).
##### CLI & Other
- The built-in `clean`, `setup`, `deploy`, and `rebuild` commands now prefer user scripts over built-in commands. When a project's `package.json` has a script with the same name, `pnpm` executes the script instead of the built-in command. Added `purge` as an alias for the built-in `clean` command, which always runs the built-in regardless of scripts [#​11118](https://github.com/pnpm/pnpm/pull/11118).
- Added `-F` as a short alias for the `--filter` option.
- Added support for hidden scripts. Scripts starting with `.` are hidden and cannot be run directly via `pnpm run`. They can only be called from other scripts. Hidden scripts are also omitted from the `pnpm run` listing [#​11041](https://github.com/pnpm/pnpm/pull/11041).
- `pnpm approve-builds` now accepts positional arguments for approving or denying packages without the interactive prompt. Prefix a package name with `!` to deny it. Only mentioned packages are affected; the rest are left untouched [#​11030](https://github.com/pnpm/pnpm/pull/11030).
- During install, packages with ignored builds that are not yet listed in `allowBuilds` are automatically added to `pnpm-workspace.yaml` with a placeholder value, so users can manually set them to `true` or `false` [#​11030](https://github.com/pnpm/pnpm/pull/11030).
- Added `pn` and `pnx` short aliases for `pnpm` and `pnpx` (`pnpm dlx`) [#​11052](https://github.com/pnpm/pnpm/pull/11052).
- `pnpm store prune` now displays the total size of removed files [#​11047](https://github.com/pnpm/pnpm/pull/11047).
- `pnpm audit --fix` now adds the minimum patched version for each advisory to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml`, so the security fix can be installed without waiting for `minimumReleaseAge` [#​11216](https://github.com/pnpm/pnpm/pull/11216).
- pnpm now warns when `optimisticRepeatInstall` skips `shouldRefreshResolution` hooks [#​10995](https://github.com/pnpm/pnpm/pull/10995).
##### Performance
- Replaced `node-fetch` with native `undici` for HTTP requests throughout pnpm [#​10537](https://github.com/pnpm/pnpm/pull/10537).
- Eliminated redundant internal linking during GVS warm reinstall when no packages were added [#​11073](https://github.com/pnpm/pnpm/pull/11073).
- Eliminated the staging directory when importing packages into `node_modules`, avoiding the overhead of creating a temp dir and renaming per package [#​11088](https://github.com/pnpm/pnpm/pull/11088).
- CAS files are now written directly to their final content-addressed path instead of to a temp file and renamed. This eliminates \~30k rename syscalls per cold install [#​11087](https://github.com/pnpm/pnpm/pull/11087).
- Optimized hot-path string operations in the content-addressable store and increased `gunzipSync` chunk size for fewer buffer allocations during tarball decompression [#​11086](https://github.com/pnpm/pnpm/pull/11086).
- Improved HTTP performance with Happy Eyeballs (dual-stack), better keep-alive settings, and an optimized global dispatcher. Tarball downloads with known size now pre-allocate memory to avoid double-copy overhead [#​11151](https://github.com/pnpm/pnpm/pull/11151).
- Adopted `If-Modified-Since` for conditional metadata fetches, avoiding re-downloading unchanged registry metadata [#​11161](https://github.com/pnpm/pnpm/pull/11161).
- Switched to abbreviated metadata when checking `minimumReleaseAge`, reducing the amount of data fetched from the registry [#​11160](https://github.com/pnpm/pnpm/pull/11160).
- Switched the metadata cache to NDJSON format, improving read/write performance [#​11188](https://github.com/pnpm/pnpm/pull/11188).
#### Patch Changes
- Switched to `process.stderr.write` instead of `console.error` for script logging [#​11140](https://github.com/pnpm/pnpm/pull/11140).
- Respected the `frozen-lockfile` flag when migrating config dependencies [#​11067](https://github.com/pnpm/pnpm/pull/11067).
- Removed the `--workspace` flag from the `version` command [#​11115](https://github.com/pnpm/pnpm/pull/11115).
- Handled `ENOTSUP` error in the clone import path during parallel I/O [#​11117](https://github.com/pnpm/pnpm/pull/11117).
- Fixed `pnpm audit` command.
- Updated dependencies to fix vulnerabilities.
- pnpm now checks whether a package is installable for non-npm-hosted packages (e.g., git or tarball dependencies) after the manifest has been fetched.
- pnpm now explicitly passes the path of the global `rc` config file to `npm`.
- Fixed YAML formatting preservation in `pnpm-workspace.yaml` when running commands like `pnpm update`. Previously, quotes and other formatting were lost even when catalog values didn't change.
Closes [#​10425](https://github.com/pnpm/pnpm/issues/10425)
- The parameter set by the `--allow-build` flag is now written to `allowBuilds`.
- Fixed a bug in which specifying `filter` in `pnpm-workspace.yaml` would cause pnpm to not detect any projects.
- Deferred patch errors until all patches in a group are applied, so that one failed patch does not prevent other patches from being attempted.
- pnpm now fails on incompatible lockfiles in CI when frozen lockfile mode is enabled [#​10978](https://github.com/pnpm/pnpm/pull/10978).
- Fixed `strictDepBuilds` and `allowBuilds` checks being bypassed when a package's build side-effects are cached in the store [#​11039](https://github.com/pnpm/pnpm/pull/11039).
- In GVS mode, `pnpm approve-builds` now runs a full install instead of rebuild, ensuring that GVS hash directories and symlinks are updated correctly after changing `allowBuilds` [#​11043](https://github.com/pnpm/pnpm/pull/11043).
- Fixed a crash in the lockfile merger when merging non-semver version strings (e.g. `link:`, `file:`, git URLs) [#​11102](https://github.com/pnpm/pnpm/pull/11102).
- Handled `ENOTSUP` error in `linkOrCopy` during parallel imports [#​11103](https://github.com/pnpm/pnpm/pull/11103).
- Skipped linking bins that already reference the correct target. This avoids redundant I/O during repeated installs and prevents permission errors when the store is read-only (e.g. Docker layer caching, CI prewarm, NFS) [#​11069](https://github.com/pnpm/pnpm/pull/11069).
- Fixed `_password` handling for the default registry to decode from base64 before use, consistent with scoped registry behavior [#​11089](https://github.com/pnpm/pnpm/pull/11089).
- Fixed a bug where the CAS locker cache was not updated when a file already existed with correct integrity [#​11085](https://github.com/pnpm/pnpm/pull/11085).
- Prevented catalog entries from being removed by `cleanupUnusedCatalogs` when they are referenced only from workspace `overrides` [#​11075](https://github.com/pnpm/pnpm/pull/11075).
- Resolved patch file paths during `pnpm fetch` [#​11054](https://github.com/pnpm/pnpm/pull/11054).
- Fixed invalid specifiers for peers on all non-exact version selectors [#​11049](https://github.com/pnpm/pnpm/pull/11049).
- Fixed false "Command not found" error on Windows when the command exists but exits with a non-zero exit code [#​11000](https://github.com/pnpm/pnpm/issues/11000).
- Prepended `Bearer` to the authorization token generated by `tokenHelper` if it is missing, aligning with npm's behavior [#​11097](https://github.com/pnpm/pnpm/pull/11097).
- Propagated error cause when throwing `PnpmError` in `@pnpm/npm-resolver` [#​10990](https://github.com/pnpm/pnpm/pull/10990).
- Fixed SQLite race condition during store initialization on Windows.
- Removed `rimrafSync` in `importIndexedDir` fast-path error handler [#​11168](https://github.com/pnpm/pnpm/pull/11168).
- Fixed `pnpm dedupe --check` unexpectedly failing due to non-deterministic resolution [#​11110](https://github.com/pnpm/pnpm/pull/11110).
- Fixed empty files not being rejected in `isEmptyDirOrNothing` [#​11182](https://github.com/pnpm/pnpm/pull/11182).
- Fixed `.bat`/`.cmd` token helpers not working on Windows due to missing `shell: true` option.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.34.5`](https://github.com/pnpm/pnpm/releases/tag/v10.34.5): pnpm 10.34.5
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.4...v10.34.5)
#### Patch Changes
- [`78e29fe`](https://github.com/pnpm/pnpm/commit/78e29fe): Prevent a crafted `pnpm-lock.yaml` from writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g. `../../../tmp/x@1.0.0`) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshot `version: "../../x"`) is now rejected at `iterateHashedGraphNodes`, the single point every global-virtual-store slot path funnels through.
- [`78e29fe`](https://github.com/pnpm/pnpm/commit/78e29fe): Fixed a path traversal vulnerability where a dependency whose manifest `name` was a scoped path traversal (e.g. `@x/../../../<path>`) could be written outside `node_modules` to an attacker-controlled location during `pnpm install`, even with `--ignore-scripts`. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.
- [`47ef6f0`](https://github.com/pnpm/pnpm/commit/47ef6f0): Fixed switching to and self-updating to pnpm v12. pnpm v12 (the Rust port) ships as the `pnpm` and `@pnpm/exe` npm packages whose bins are placeholders replaced at install time by the host's native binary from a `@pnpm/exe.<platform>-<arch>[-musl]` optional dependency. Because pnpm installs its own engine with `--ignore-scripts`, that relinking never ran, leaving a non-executable placeholder. pnpm now relinks the native binary itself for v12 (recognizing the new platform-package naming scheme and the native `pnpm` package), and verifies the native binary's npm registry signature before running it.
- [`36928be`](https://github.com/pnpm/pnpm/commit/36928be): `${...}` environment-variable placeholders in the `httpProxy`, `httpsProxy`, `noProxy`, `proxy`, and `noproxy` settings are no longer expanded when these settings come from a project's `pnpm-workspace.yaml`. They now receive the same protection already applied to `registry`.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.34.4`](https://github.com/pnpm/pnpm/releases/tag/v10.34.4): pnpm 10.34.4
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.3...v10.34.4)
#### Patch Changes
- [`352ae48`](https://github.com/pnpm/pnpm/commit/352ae48): Security: validate config dependency names and versions before using them to build filesystem paths. A `pnpm-workspace.yaml` with a traversal-shaped `configDependencies` name (such as `../../PWNED`) or version (such as `../../../PWNED`) could previously cause `pnpm install` to create symlinks or write package files outside `node_modules/.pnpm-config` and the store. Names must now be valid npm package names and versions must be exact semver versions. See [GHSA-qrv3-253h-g69c](https://github.com/pnpm/pnpm/security/advisories/GHSA-qrv3-253h-g69c).
- [`352ae48`](https://github.com/pnpm/pnpm/commit/352ae48): Reject path-traversal and reserved dependency aliases (such as `../../../escape`, `.bin`, `.pnpm`, or `node_modules`) that come from a lockfile rather than a freshly resolved manifest. A crafted lockfile alias could otherwise be joined directly under a hoisted `node_modules` directory, letting package files be written outside the intended install root or overwrite pnpm-owned layout.
The `nodeLinker: hoisted` graph builder now validates each alias at the directory sink (`safeJoinModulesDir`), matching the validation pnpm already performs when resolving aliases from manifests. See [GHSA-fr4h-3cph-29xv](https://github.com/pnpm/pnpm/security/advisories/GHSA-fr4h-3cph-29xv).
- [`352ae48`](https://github.com/pnpm/pnpm/commit/352ae48): Prevent `pnpm patch-remove` from removing files outside the configured patches directory.
- [`217fbe0`](https://github.com/pnpm/pnpm/commit/217fbe0): Hardened the warning printed when a project `.npmrc` uses environment variables in registry/auth settings: the suggested `pnpm config set` command is now only included for keys made up of shell-inert characters. Because the key comes from a repository-controlled `.npmrc` and a shell expands `$(...)`, backticks, and `$VAR` even inside double quotes, a crafted key could otherwise have turned the suggested copy-paste command into command execution.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.34.3`](https://github.com/pnpm/pnpm/releases/tag/v10.34.3): pnpm 10.34.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.2...v10.34.3)
##### ⚠️ Security fix — environment variables in a project `.npmrc` (action may be required)
Following [GHSA-3qhv-2rgh-x77r](https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r), pnpm no longer expands `${ENV_VAR}` placeholders that come from a **repository-controlled** config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
- the project/workspace `.npmrc` — `registry`, `@scope:registry`, proxy URLs, URL-scoped keys (`//host/…`), and credential values (`_authToken`, `_auth`, `_password`, `username`, `tokenHelper`, `cert`, `key`);
- registry URLs in `pnpm-workspace.yaml`.
This release also closes a bypass where a project `.npmrc` could set `userconfig`, `globalconfig`, or `prefix` to make pnpm load a repo-supplied file as *trusted* config (via `@pnpm/npm-conf@3.0.3`).
Environment variables are **still** expanded in trusted config: your user-level `~/.npmrc`, the global config, CLI options, and environment config.
**If your authentication broke after upgrading**, move the token out of the committed `.npmrc`:
```sh
# Writes to your user/global config, not the repository:
pnpm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
```
Or keep the `${NPM_TOKEN}` line but put it in your user-level `~/.npmrc` instead of the repo. In **GitHub Actions**, `actions/setup-node` with `registry-url` already writes a user-level `.npmrc`, so `NODE_AUTH_TOKEN` keeps working. For other CI where editing each pipeline is hard, set `NPM_CONFIG_USERCONFIG=.npmrc` in the CI environment to declare the project `.npmrc` trusted.
See <https://pnpm.io/npmrc> for full migration details.
#### Patch Changes
- Improved the warning printed when a project `.npmrc` uses an environment variable in a registry/proxy URL or in registry credentials. The message now explains why the setting was ignored and how to migrate it to a trusted source — for example by running `pnpm config set "<key>" <value>` to store it in the global config, or by keeping the `${...}` line in the user-level `~/.npmrc` — with a link to <https://pnpm.io/npmrc>.
- A repository-controlled project or workspace `.npmrc` can no longer redirect which files pnpm loads as its trusted user and global configuration. Previously such a file could set `userconfig`, `globalconfig`, or `prefix` to point at an attacker-supplied file shipped in the repository, and pnpm would load it as a trusted config source — bypassing the protection that prevents repository config from expanding environment variables into registry request destinations and credentials, and allowing it to set `tokenHelper`. The user/global config file locations are now resolved only from trusted sources (CLI options, environment config, the npm builtin config, and defaults) before the project and workspace `.npmrc` files are read. Fixed by upgrading `@pnpm/npm-conf` to `3.0.3`.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.34.2`](https://github.com/pnpm/pnpm/releases/tag/v10.34.2): pnpm 10.34.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.1...v10.34.2)
##### ⚠️ Security fix — environment variables in a project `.npmrc` (action may be required)
Following [GHSA-3qhv-2rgh-x77r](https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r), pnpm no longer expands `${ENV_VAR}` placeholders that come from a **repository-controlled** config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:
- the project/workspace `.npmrc` — `registry`, `@scope:registry`, proxy URLs, URL-scoped keys (`//host/…`), and credential values (`_authToken`, `_auth`, `_password`, `username`, `tokenHelper`, `cert`, `key`);
- registry URLs in `pnpm-workspace.yaml`.
This release also closes a bypass where a project `.npmrc` could set `userconfig`, `globalconfig`, or `prefix` to make pnpm load a repo-supplied file as *trusted* config (via `@pnpm/npm-conf@3.0.3`).
Environment variables are **still** expanded in trusted config: your user-level `~/.npmrc`, the global config, CLI options, and environment config.
**If your authentication broke after upgrading**, move the token out of the committed `.npmrc`:
```sh
# Writes to your user/global config, not the repository:
pnpm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN"
```
Or keep the `${NPM_TOKEN}` line but put it in your user-level `~/.npmrc` instead of the repo. In **GitHub Actions**, `actions/setup-node` with `registry-url` already writes a user-level `.npmrc`, so `NODE_AUTH_TOKEN` keeps working. For other CI where editing each pipeline is hard, set `NPM_CONFIG_USERCONFIG=.npmrc` in the CI environment to declare the project `.npmrc` trusted.
See <https://pnpm.io/npmrc> for full migration details.
#### Patch Changes
- Package-manager bootstrap traffic is now resolved through trusted registries and trusted network config. When pnpm downloads the pnpm version requested by a repository's `packageManager` field, the registry it fetches from (and the proxy/TLS settings used for that traffic) now come exclusively from trusted config sources — CLI options, env config, user and global `.npmrc` — defaulting to the public npm registry, instead of the repository's project/workspace settings.
- pnpm now verifies the npm registry signature of a package-manager binary before spawning it. When the `packageManager` field (or `pnpm self-update`) makes pnpm download another pnpm version, the staged install is verified corepack-style: the integrity recorded in the staged lockfile must carry a valid npm registry signature for the exact `name@version`, validated against npm's public signing keys that ship embedded in the pnpm CLI. Verification fails closed — a tampered download, an unsigned package, or an unreachable registry refuses the version switch rather than running an unverified binary. It runs only when the wanted version is actually downloaded (a tools-directory cache miss), so repeated commands pay no extra network round trip.
- Environment variable expansion is now trust-aware for registry/auth config and request destinations. Repository-controlled config files (the project and workspace `.npmrc` and `pnpm-workspace.yaml`) can no longer expand `${...}` placeholders in registry/proxy request destinations, URL-scoped keys, or registry credential values, preventing repository-controlled configuration from exfiltrating environment secrets through request URLs. Trusted user/global/CLI/env config keeps full env expansion, so existing token and registry setup flows continue to work.
- Reject reserved manifest `bin` names (`""`, `"."`, `".."`, and scoped forms such as `@scope/..`) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted.
- Require trusted package identity before package-name `onlyBuiltDependencies` (and `allowBuilds`) entries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as the key. Lockfile entries are now rejected when a registry-style dependency path (`name@semver`) is backed by a git, directory, or git-hosted tarball resolution (`ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`), so the dependency path is a reliable artifact identity by the time scripts can run.
- pnpm now verifies the detached OpenPGP signature of a Node.js release's `SHASUMS256.txt` against the Node.js release team's public keys (embedded in the pnpm CLI) before trusting its hashes. The Node.js download mirror is repository-configurable (`node-mirror:<channel>` in `.npmrc`), and the integrity check previously trusted a `SHASUMS256.txt` fetched from that same mirror — a circular check that a malicious mirror could satisfy with a tampered binary and matching hashes. A mirror that proxies the real signed SHASUMS keeps working unchanged. Only the `release` channel publishes signed SHASUMS files, so pre-release channels (rc, nightly, …) remain unverified.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.34.1`](https://github.com/pnpm/pnpm/releases/tag/v10.34.1): pnpm 10.34.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.34.0...v10.34.1)
#### Patch Changes
- Reject `pnpm-lock.yaml` entries whose remote tarball `resolution:` block is missing the `integrity` field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips `integrity:`) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under `--frozen-lockfile`. pnpm now fails closed at lockfile-read time with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. Git-hosted tarballs (`gitHosted: true` or a URL on codeload.github.com / bitbucket.org / gitlab.com) and `file:` tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.34.0`](https://github.com/pnpm/pnpm/releases/tag/v10.34.0): pnpm 10.34
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.33.4...v10.34.0)
#### Minor Changes
- Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, `pnpm install` (non-frozen) would log `ERR_PNPM_TARBALL_INTEGRITY`, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.
`pnpm install` now exits with `ERR_PNPM_TARBALL_INTEGRITY` and a hint pointing at the new opt-in flag.
The only opt-in is **`pnpm install --update-checksums`** — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.
`--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass.
#### Patch Changes
- Pin unscoped per-registry settings (`_authToken`, `_auth`, `username`/`_password`, `tokenHelper`, inline `cert`/`key`) to the registry declared in the same config source at load time, so a later layer overriding `registry=` (workspace `.npmrc`, `pnpm-workspace.yaml`, CLI `--registry`) cannot redirect a credential or client certificate authored for a different host. A deprecation warning is emitted whenever an unscoped per-registry setting is encountered, naming the source and the URL it was pinned to. Reported by JUNYI LIU.
- Fixed `minimumReleaseAge` handling when cached metadata is abbreviated. The npm registry returns abbreviated package metadata (without the per-version `time` field) by default, which made the maturity check throw `ERR_PNPM_MISSING_TIME` whenever cached abbreviated metadata was reused. pnpm now upgrades cached abbreviated metadata to the full document via a follow-up fetch when `minimumReleaseAge` is active, persists the upgrade to the on-disk cache so subsequent installs skip the extra fetch, and lets `ERR_PNPM_MISSING_TIME` from the cache fast-path fall through to the network fetch even under strict mode.
- Reject git resolutions whose `commit` field is not a 40-character hexadecimal SHA before invoking `git`. A malicious lockfile could otherwise smuggle a value such as `--upload-pack=<command>` through `git fetch` / `git checkout`, which on SSH or local-file transports executes the supplied command.
- Reject patch files whose `diff --git` headers reference paths outside the patched package directory. Previously a malicious `.patch` file added via a pull request could write, delete, or rename arbitrary files reachable by the user running `pnpm install`.
- Fixed `--prefix=<dir>` not being honored when locating the workspace root. The `--prefix → dir` rename was applied after workspace detection, so workspace settings declared in `<dir>/pnpm-workspace.yaml` were not loaded when pnpm was invoked from outside `<dir>` [#​11535](https://github.com/pnpm/pnpm/issues/11535).
- Reject dependency aliases that contain path-traversal segments (such as `@x/../../../../../.git/hooks`) when reading them from a package manifest or symlinking them into `node_modules`. A malicious registry package could otherwise use a transitive dependency key to make `pnpm install` create symlinks at attacker-chosen paths outside the intended `node_modules` directory.
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.33.4`](https://github.com/pnpm/pnpm/releases/tag/v10.33.4): pnpm 10.33.4
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.33.3...v10.33.4)
#### Patch Changes
- Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
A new `gitHosted: true` field is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.
- Fix a regression where `pnpm --recursive --filter '!<pkg>' run/exec/test/add` would include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative `--filter` arguments are provided, matching the [documented behavior](https://pnpm.io/cli/recursive). To include the root, pass `--include-workspace-root` [#​11341](https://github.com/pnpm/pnpm/issues/11341).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.33.3`](https://github.com/pnpm/pnpm/releases/tag/v10.33.3): pnpm 10.33.3
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.33.2...v10.33.3)
#### Patch Changes
- When self-updating from v10's `@pnpm/exe` to v11+ on Intel macOS (darwin-x64), `pnpm self-update` now transparently switches to the JS-only `pnpm` package on npm instead of installing `@pnpm/exe@v11+` (which doesn't ship a working binary for Intel Macs because of an upstream Node.js SEA bug — see [#​11423](https://github.com/pnpm/pnpm/issues/11423) and [nodejs/node#62893](https://github.com/nodejs/node/issues/62893)). Without this, the self-update would silently leave the user with no working `pnpm` binary. The new install requires Node.js to be available on `PATH`; a warning is printed when the swap happens. All other host/version combinations are unchanged.
- `pnpm self-update` (with no version argument) no longer downgrades pnpm when the registry's `latest` dist-tag points to an older release than the currently active version. Run `pnpm self-update latest` to force a downgrade [#​11418](https://github.com/pnpm/pnpm/issues/11418).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.33.2`](https://github.com/pnpm/pnpm/releases/tag/v10.33.2): pnpm 10.33.2
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.33.1...v10.33.2)
#### Patch Changes
- Globally-installed bins no longer fail with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` when pnpm was installed via the standalone `@pnpm/exe` binary (e.g. `curl -fsSL https://get.pnpm.io/install.sh | sh -`) on a system without a separate Node.js installation. Previously, when `which('node')` failed during `pnpm add --global`, pnpm fell back to `process.execPath`, which in `@pnpm/exe` is the pnpm binary itself — and that path was baked into the generated bin shim, causing the shim to invoke pnpm instead of Node [#​11291](https://github.com/pnpm/pnpm/issues/11291), [#​4645](https://github.com/pnpm/pnpm/issues/4645).
- Fix an infinite fork-bomb that could happen when pnpm was installed with one version (e.g. `npm install -g pnpm@A`) and run inside a project whose `package.json` selected a different pnpm version via the `packageManager` field (e.g. `pnpm@B`), while a `pnpm-workspace.yaml` also existed at the project root.
The child's environment is now forced to `manage-package-manager-versions=false` (v10) and `pm-on-fail=ignore` (v11+), which disables the package-manager-version handling in whichever pnpm runs as the child.
Fixes [#​11337](https://github.com/pnpm/pnpm/issues/11337).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
### [`v10.33.1`](https://github.com/pnpm/pnpm/releases/tag/v10.33.1): pnpm 10.33.1
[Compare Source](https://github.com/pnpm/pnpm/compare/v10.33.0...v10.33.1)
#### Patch Changes
- When a project's `packageManager` field selects pnpm v11 or newer, commands that v10 would have passed through to npm (`version`, `login`, `logout`, `publish`, `unpublish`, `deprecate`, `dist-tag`, `docs`, `ping`, `search`, `star`, `stars`, `unstar`, `whoami`, etc.) are now handed over to the wanted pnpm, which implements them natively. Previously they silently shelled out to npm — making, for example, `pnpm version --help` print npm's help on a project with `packageManager: pnpm@11.0.0-rc.3` [#​11328](https://github.com/pnpm/pnpm/issues/11328).
<!-- sponsors -->
#### Platinum Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
</td>
</tr>
</tbody>
</table>
#### Gold Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://discord.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
</picture>
</a>
</td>
</tr>
<tr>
<td align="center" valign="middle">
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
</picture>
</a>
</td>
<td align="center" valign="middle">
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=release_notes" target="_blank">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
</picture>
</a>
</td>
</tr>
</tbody>
</table>
<!-- sponsors end -->
</details>
<details>
<summary>microsoft/TypeScript (typescript)</summary>
### [`v7.0.2`](https://github.com/microsoft/TypeScript/releases/tag/v7.0.2): TypeScript 7.0.2
[Compare Source](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)
<https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/>
This tag was originally released at: <https://github.com/microsoft/typescript-go/releases/tag/typescript%2Fv7.0.2>
### [`v6.0.3`](https://github.com/microsoft/TypeScript/releases/tag/v6.0.3): TypeScript 6.0.3
[Compare Source](https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3)
For release
> ❗ **Important**
>
> ✂ PR body was truncated to here.
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41OS43IiwidXBkYXRlZEluVmVyIjoiNDQuMTE1LjEzIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
This PR contains the following updates:
^25.0.0→^26.0.0^5.0.0→^6.0.0v6→v725→26v6→v7v3→v4v5→v6v3→v4v3→v4^28.0.0→^30.0.010.33.0→12.6.0^5.7.3→^7.0.0^4.0.0→^5.0.0Release Notes
vitejs/vite-plugin-react (@vitejs/plugin-react)
v6.1.1Compare Source
Add
compiler.logDiagnosticsoptionRecoverable React Compiler diagnostics are no longer logged by default. Set
compiler.logDiagnosticstotrueto log them through Vite. Fatal diagnostics are always logged and fail the transform.Respect environment sourcemap option for React Compiler transform when
builder.sharedPluginsis enabled (#1439)The React Compiler transform was using the top-level sourcemap option instead of the environment sourcemap option. This caused a problem when the experimental
builder.sharedPluginswas enabled.v6.1.0Compare Source
Add experimental native React Compiler support (#1419)
Add experimental native React Compiler support.
You can use it by installing
oxc-transform-reactand enabling it via thecompileroption:v6.0.5Compare Source
Fixed the react compiler preset filter to be linear (#1353)
The improved filter in v6.0.3 was non-linear and caused a performance regression (#1349). The filter was changed to be linear to avoid that.
v6.0.4Compare Source
Fixed
$RefreshSig$ is not definederror when runningvite devwithNODE_ENV=productionWhen running
vite devwithNODE_ENV=production, the app errored with$RefreshSig$ is not defined.This error is now fixed.
v6.0.3Compare Source
Improve the react compiler preset filter to reduce false-positives (#1138)
Improved the filter in the react compiler babel preset to reduce the false-positives so that less modules are processed by the react compiler.
v6.0.2Compare Source
Allow all options in reactCompilerPreset (#1189)
This is a type only change. Only
compilationModeandtargetoptions were available forreactCompilerPreset.v6.0.1Compare Source
Expand
@rolldown/plugin-babelpeer dep range (#1146)Expanded
@rolldown/plugin-babelpeer dep range to include^0.2.0.v6.0.0Compare Source
actions/checkout (actions/checkout)
v7.0.1Compare Source
v7.0.0Compare Source
docker/build-push-action (docker/build-push-action)
v7.4.0Compare Source
Full Changelog: https://github.com/docker/build-push-action/compare/v7.3.0...v7.4.0
v7.3.0Compare Source
Full Changelog: https://github.com/docker/build-push-action/compare/v7.2.0...v7.3.0
v7.2.0Compare Source
Full Changelog: https://github.com/docker/build-push-action/compare/v7.1.0...v7.2.0
v7.1.0Compare Source
Full Changelog: https://github.com/docker/build-push-action/compare/v7.0.0...v7.1.0
v7.0.0Compare Source
DOCKER_BUILD_NO_SUMMARYandDOCKER_BUILD_EXPORT_RETENTION_DAYSenvs by @crazy-max in #1473Full Changelog: https://github.com/docker/build-push-action/compare/v6.19.2...v7.0.0
docker/login-action (docker/login-action)
v4.6.0Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v4.5.2...v4.6.0
v4.5.2Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v4.5.1...v4.5.2
v4.5.1Compare Source
dhi.ioas Docker Hub OIDC registry by @crazy-max in #1054Full Changelog: https://github.com/docker/login-action/compare/v4.5.0...v4.5.1
v4.5.0Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v4.4.0...v4.5.0
v4.4.0Compare Source
registry-authsecret mask by @crazy-max in #1035Full Changelog: https://github.com/docker/login-action/compare/v4.3.0...v4.4.0
v4.3.0Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v4.2.0...v4.3.0
v4.2.0Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v4.1.0...v4.2.0
v4.1.0Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v4.0.0...v4.1.0
v4.0.0Compare Source
Full Changelog: https://github.com/docker/login-action/compare/v3.7.0...v4.0.0
docker/metadata-action (docker/metadata-action)
v6.2.0Compare Source
Full Changelog: https://github.com/docker/metadata-action/compare/v6.1.0...v6.2.0
v6.1.0Compare Source
Full Changelog: https://github.com/docker/metadata-action/compare/v6.0.0...v6.1.0
v6.0.0Compare Source
#inside values while still supporting full-line#comments by @crazy-max in #607Full Changelog: https://github.com/docker/metadata-action/compare/v5.10.0...v6.0.0
docker/setup-buildx-action (docker/setup-buildx-action)
v4.4.1Compare Source
Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.4.0...v4.4.1
v4.4.0Compare Source
Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.3.0...v4.4.0
v4.3.0Compare Source
Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.2.0...v4.3.0
v4.2.0Compare Source
Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.1.0...v4.2.0
v4.1.0Compare Source
Full Changelog: https://github.com/docker/setup-buildx-action/compare/v4.0.0...v4.1.0
v4.0.0Compare Source
Full Changelog: https://github.com/docker/setup-buildx-action/compare/v3.12.0...v4.0.0
docker/setup-qemu-action (docker/setup-qemu-action)
v4.4.0Compare Source
Full Changelog: https://github.com/docker/setup-qemu-action/compare/v4.3.0...v4.4.0
v4.3.0Compare Source
Full Changelog: https://github.com/docker/setup-qemu-action/compare/v4.2.0...v4.3.0
v4.2.0Compare Source
Full Changelog: https://github.com/docker/setup-qemu-action/compare/v4.1.0...v4.2.0
v4.1.0Compare Source
resetinput to uninstall current emulators by @crazy-max in #21Full Changelog: https://github.com/docker/setup-qemu-action/compare/v4.0.0...v4.1.0
v4.0.0Compare Source
Full Changelog: https://github.com/docker/setup-qemu-action/compare/v3.7.0...v4.0.0
jsdom/jsdom (jsdom)
v30.1.1Compare Source
blurandfocusoutevents and incorrectevent.relatedTargetvalues when focusing an element after removing the previously focused element, which regressed in v30.1.0. (@asamuzaK)document.documentElement.focus(). (@asamuzaK)blurlisteners becoming active, and text selections made by focus and blur listeners being overwritten. (@asamuzaK)element.focus()incorrectly focusing disabled form controls and<input type="hidden">elements withtabindex="". (@scttcper)style.setProperty()calls changing existing!importantpriorities, serialized styles, or mutation records. (@FedgeNo)!importanthandling when updating CSS longhands after shorthands, using variables or CSS-wide keywords, and assigning style properties directly. (@FedgeNo)<noscript>parsing withincludeNodeLocations: trueor inside frames to honor therunScriptsoption.storageQuotaoption being ignored by frames.charsetparameters in<meta>elements, and encoding detection incorrectly using incomplete<meta>tags. (@FedgeNo)constructor,toString,__proto__, or"null", and incorrect reuse of namespace prefixes declared on sibling elements.element.innerHTMLandelement.outerHTMLin XML documents to reject invalid characters in attribute values and avoid stack overflows on large strings.:lang(),:nth-child(... of ...)after mutations, and:has()with duplicate IDs or nested logical pseudo-classes. (@asamuzaK)v30.1.0Compare Source
jsdom is feeling the AGI!
This release is dedicated to @scttcper, who unleashed @codex upon jsdom and found tons of performance improvements. Along the way, he found and fixed many correctness issues as well.
We really appreciate his thoughtful PRs, which did a great job following the project's contribution guidelines, and were clearly human-curated, with their PR descriptions edited to be brief and respectful of the maintainers' time.
Thanks to @scttcper, as well as all the other contributors of this release (most of whom were AI-assisted).
document, such asdocument.myFormfor<form name="myForm">. (@vojtisprime11)QuotaExceededError, including its use for storage quota errors and oversizedcrypto.getRandomValues()requests.getComputedStyle(), style changes, and CSS serialization. (@scttcper, @jhult)<select>elements and radio button groups. (@scttcper)window.close()to preserve access to the document and its DOM through retained references.element.querySelectorAll()returning no matches when the first part of the selector matches the element itself, which regressed in v30.0.0. (@asamuzaK)data-state="",title="", and other case-sensitive values. (@asamuzaK)document.querySelector()failing to find a matching element when an earlier element has the same ID but does not match the rest of the selector. (@vojtisprime11):focusmatching in shadow trees. (@asamuzaK)document.replaceChildren()calls, invalid document element and doctype placements, and mutations duringelement.replaceWith().window.close()or iframe removal, and prevented new scripts, resource loads, timers, and animation frames from starting in destroyed documents. (@scttcper)requestInterceptor()callbacks, and when reusing anXMLHttpRequestafter aborting it.JSDOM.fromURL()potentially hanging when response handling throws and response stream cleanup does not finish.getComputedStyle()anddocument.styleSheetsusing the wrong stylesheet order after inserting or updating<style>elements.getComputedStyle()ignoring nested@importand@mediarules in imported stylesheets, and returning stale results after imports finish loading.getComputedStyle()calls changing case-sensitive background URLs, and inconsistent resolution of border shorthands containing system colors. (@scttcper)16px, which regressed in v30.0.0. (@Alberto-BaseNet)getComputedStyle()to resolve'font-weight'keywords to numeric values. (@tianrking)getComputedStyle()to convert lengths to pixels inside CSS math functions containing percentages, and to resolve percentages in'font-size'math functions. (@soroushm)min()andmax()containing nestedcalc(), which regressed in v30.0.0. (@asamuzaK)'grid-template-columns'values containing both100pxandcalc(). (@rome-xi)'background'and'border'shorthands with adjacent components, such asurl(a.png)no-repeat, including a crash when parsing inline styles. Also fixed handling of invalid shorthand assignments and escaped or unusual characters in CSS declarations. (@asamuzaK)'flex'shorthands, such as35 1 0, and rejection of negative'flex-basis'lengths and percentages. (@asamuzaK)RangeandSelectionhandling of CDATA sections, including boundary offsets and range cloning, extraction, deletion, insertion, and stringification. (@scttcper)text.normalize()incorrectly removing the text node or merging its siblings. (@scttcper)window, and incorrect named access from empty or namespacedid=""andname=""values. (@scttcper)DOMParserordocument.implementation.createHTMLDocument()appearing as named properties onwindowand being retained in memory. (@Iaotle)storageevents being sent to windows created after the storage change, and ensured surviving recipients still receive events when the source document is destroyed.input.listin detached element trees. (@scttcper)attr.ownerDocumentafter setting an attribute node on an element in another document or adopting its element. (@Kjubikstronk)element.getElementsByTagName()calls using the previous document's case-sensitivity rules after adoption between HTML and XML documents. (@Kjubikstronk)element.tagNamereturning stale casing after adoption between HTML and XML documents.<select>selection updates when moving options, and unintended selection resets when moving a whole<select>or changing unrelated descendants.<input>elements to preserveinput.indeterminate.<script>. (@Kjubikstronk)src="", and handling oftype="",for="", andevent="".document.currentScriptduring nested script execution and scripts in shadow trees.window.event, and default passive listener handling for non-node event targets, and event dispatch when user code modifieswindow.constructor.volumechangeandratechangeevents to fire asynchronously. (@christianaurichzm)NodeIteratortraversal when its filter removes nodes.fileReader.readAsText()to honor the blob's MIME type charset when no supported explicit encoding is supplied.input.valueAsNumberassignments for time and local date/time inputs.<textarea>values to use the numerictextarea.colsvalue.document.readyStateto be"complete"for documents created withdocument.implementation.createDocument()anddocument.implementation.createHTMLDocument().element.translatefor emptytranslate=""values and elements without a parent.node.lookupNamespaceURI()for thexmlandxmlnsprefixes. (@vojtisprime11)<base>elements to ignoredata:andjavascript:URLs. (@vojtisprime11)svgElement.viewportElementinside<symbol>elements.blobEvent.timecodeto beNaN.CSS.v30.0.1Compare Source
getComputedStyle()withcalc()and other functions throwing an exception, which regressed in v30.0.0. (@asamuzaK)v30.0.0Compare Source
Breaking changes:
^22.22.2 || ^24.15.0 || >=26.0.0.Other changes:
CSS.escape()andCSS.supports()functions. (@asamuzaK)'background-position-x'and'background-position-y'CSS properties. (@olagokemills)getComputedStyle()to convert length values into pixels. (@asamuzaK)getPropertyValue(). (@asamuzaK)document.evaluate()(@dokson)v29.1.1Compare Source
'border-radius'computed style serialization. (@asamuzaK)'background-origin'and'background-clip'CSS properties. (@asamuzaK)getComputedStyle(), before the cache warms up. (@asamuzaK)v29.1.0Compare Source
getComputedStyle()sometimes returning outdated results after CSS was modified. (@asamuzaK)v29.0.2Compare Source
getComputedStyle(). Computed value rules are now applied across a broader set of properties, and include fixes related to inheritance, defaulting keywords, custom properties, and color-related values such ascurrentcolorand system colors. (@asamuzaK)'background' and'border'shorthand parsing. (@asamuzaK)v29.0.1Compare Source
'border','background', and their sub-shorthands containing keywords orvar(). (@asamuzaK)getComputedStyle()to return a more functionalCSSStyleDeclarationobject, including indexed access support, which regressed in v29.0.0.v29.0.0Compare Source
Breaking changes:
Other changes:
@acemir/cssomandcssstyledependencies with fresh internal implementations built on webidl2js wrappers and thecss-treeparser. Serialization, parsing, and API behavior is improved in various ways, especially around edge cases.CSSCounterStyleRuleandCSSNamespaceRuleto jsdomWindows.cssMediaRule.matchesandcssSupportsRule.matchesgetters.MediaList, usingcss-treeinstead of naive comma-splitting. Invalid queries become"not all"per spec.cssKeyframeRule.keyTextgetter/setter validation.cssStyleRule.selectorTextsetter validation: invalid selectors are now rejected.styleSheet.ownerNode,styleSheet.href, andstyleSheet.title.Documentinitialization performance by lazily initializing the CSS selector engine, avoiding ~0.5 ms of overhead perDocument. (@thypon)CSSStyleDeclarationmodifications to properly trigger custom element reactions.@mediarule parsing.CSSStyleSheet's "disallow modification" flag not being checked in all mutation methods.XMLHttpRequest'sresponsegetter returning parsed JSON during theLOADINGstate instead ofnull.getComputedStyle()crashing in XHTML documents when stylesheets contained at-rules such as@pageor@font-face.XMLHttpRequestcaused by a race condition with the worker thread's idle timeout.pnpm/pnpm (pnpm)
v12.6.0: pnpm 12.6Compare Source
pnpm 12.6.0 ships with automatic dependency deduplication, relocatable node_modules, package.yaml manifest editing, and --save-types support.
Minor Changes
autoDedupededuplicates compatible dependency versions during installation #7258. Enable it inpnpm-workspace.yamlor usepnpm install --auto-dedupeorpnpm add --auto-dedupe. Frozen installs leave the lockfile unchanged.pnpm install,pnpm run, andpnpm execon macOS and Linux now reuse anode_modulesdirectory 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 innode_modules/.binkeep working.pnpm add --save-typessaves available@types/*packages indevDependenciesalongside registry dependencies #3868. Packages that declare bundled TypeScript types are skipped. SetsaveTypes: trueinpnpm-workspace.yamlto enable this by default.package.yamlmanifests can now be updated bypnpm add,pnpm update,pnpm remove,pnpm pkg,pnpm link,pnpm set-script, andpnpm version#2008. Existing comments and key order are preserved.Catalog entries can now use the
file:andlink:protocols #8642. A relative path or bare path in an entry, such as./tarballs/foo.tgz, is measured from the directory holdingpnpm-workspace.yaml.pnpm tasks statuslists running and waiting tasks in each concurrency group, and waiting tasks now take available slots in arrival order with higherprioritytasks 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 namedtaskstakes precedence; usepnpm pm tasks statuswhen that script exists.pnpm cache prunedeletes registry metadata cache directories that this version of pnpm can no longer read #15046.pnpm cache prune --dry-runlists what it would delete without removing anything.macosBackup.excludeModulesDirandmacosBackup.excludeStoreDiron macOS can now exclude newly created modules, virtual-store, and package-store directories from Time Machine #6440. Set either totruein global configuration or using thePNPM_CONFIG_MACOS_BACKUP_EXCLUDE_MODULES_DIRandPNPM_CONFIG_MACOS_BACKUP_EXCLUDE_STORE_DIRenvironment variables.pnpm add --tildeis now an alias for--save-prefix=~#12863. The Yarn-Tshorthand is not supported.progresssetting and--no-progressoption now turn off dependency and download progress lines #14065. Warnings, lifecycle output, and the dependency summary are still printed.Patch Changes
Security
POSIX bin shims now take
cygpathandwslpathfrom the system default path on Cygwin, MSYS2, and WSL2 so a dependency cannot redirect another package's shim #14866.pnpm installwarnings 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 installand other commands that report configuration warnings now warn when environment variables in project.npmrccredentials are ignored #15051.Installing packages
pnpm install --frozen-lockfilenow succeeds when an optional dependency was unresolvable and skipped by the install that wrote the lockfile #3960.pnpm install --frozen-lockfileno longer installs dependencies of projects removed frompnpm-workspace.yaml#15248. Missing local tarballs used only by those projects no longer fail the install.pnpm cinow emptiesnode_modulesbefore installing in a project that declares acleanscript #15276.pnpm install --forcenow re-imports every package into the virtual store #15030 and removes obsolete dependency links inside virtual-store packages when their dependencies change #15039.preinstallscript for the root project now runs before dependencies are resolved and linked #3760.pnpm installnow runspnpm:devPreinstallwhen the root project usespackage.yaml#15168.pnpm installnow enforces the root project'sengines.noderange whenengineStrictis enabled #3016.pnpm installnow uses the running Node.js whendevEngines.runtimedeclares a range withoutonFail: download#15230.pnpm installno 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 installnow installs git-hosted dependencies without preparing them when their builds are explicitly denied byallowBuilds#10522.pnpm installnow reuses an in-flight tarball download when another resolution of the same archive still needs itspackage.json#15037.pnpm install --prodno longer downloads registry packages that only a devDependency reaches #881.pnpm install --no-runtime --frozen-lockfilewithnodeLinker: hoistedno longer fails on repeated runs with a broken lockfile #15212.Resolving and linking dependencies
pnpm installandpnpm updatenow 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 installnow links workspace dependencies declared with plain version ranges whenexcludeLinksFromLockfileandlinkWorkspacePackagesare enabled #15133.pnpm installnow resolves local tarball dependencies whose absolutefile:paths contain..consistently and skips reinstallation on repeat installs #15190.pnpm installnow installs dependencies when a custom resolver returns a local or git-hosted tarball without a manifest #15016.pnpm.overridesentries written as a bare path, such as./local-dep, are now measured from the directory holdingpnpm-workspace.yaml#11131.pnpm update --no-saveno longer bypasses version-scoped overrides when a dependency selector specifies a version #14923.pnpm peers checkand strict peer dependency checks no longer reject compatible versions from named registries #15225.pnpm outdatedandpnpm update --interactive --latestnow include named-registry dependencies such aswork:2.1.0and preserve their registry prefix #15226.Workspace projects selected by
hoistPatternorpublicHoistPatternare 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 dedupeandpnpm installin projects with many convergence overrides by checking overrides concurrently #15175.minimumReleaseAgeis no longer skipped for packages served by registries returning matching ETags for abbreviated and full package metadata #14925.Running scripts and tasks
pnpm runsignal handling no longer delivers a redundant secondSIGINTto child scripts onCtrl+Cin a terminal, and properly forwards termination signals when running non-interactively without a terminal #7374.pnpm runandpnpm execin workspaces withsharedWorkspaceLockfile: falsenow verify dependencies in the selected projects rather than expecting a root workspace state #15272.pnpm testnow forwards--filterarguments to the test script when the option follows the shortcut #15217.Recursive runs now start scripts matched by a
/pattern/selector in parallel withinworkspaceConcurrency#14933.pnpm deploy,pnpm rebuild,pnpm rb, andpnpm setupnow prefer apackage.jsonscript of the same name #14976.modulesDircustom directory names now support executable lookup and CommonJS plugin resolution acrosspnpm run,pnpm exec,pnpm versionhooks, and lifecycle scripts #3604.pnpm install-testnow accepts--no-baildirectly and in recursive runs #3777.Workspace and project configuration
pnpmcommands run in a project not included in the workspace now act on that project alone #3561.pnpm-workspace.yamledits now preserve scalar YAML anchors and aliases #8245.pnpm-workspace.yamlnow expands environment variable placeholders with fallback syntax in enum-valued settings such asnodeLinker#14914.pnpmfileconfiguration now loads a.jsfile as CommonJS or an ES module, following the nearestpackage.json#15141.updateConfighook settings are now honored bypnpm peers check,why,list,ll,licenses,audit,sbom,fetch,patch,patch-commit,patch-remove,approve-builds, andruntime#15047, #15049.readPackagehook changes or removal now take added dependencies out ofpnpm-lock.yamland update dependencies when an existing lockfile is present #3735, #15136.package.yamlprojects now record their pinned pnpm underpackageManagerDependenciesinpnpm-lock.yaml#15167.packageManagerDependenciespinning@pnpm/exebesidepnpmis no longer rewritten inpnpm-lock.yaml#14926.pnpmnow preserves CRLF line endings when modifying project manifests #3529.loglevelsetting is now honored when configured inpnpm-workspace.yaml, global configuration, orPNPM_CONFIG_LOGLEVEL#3122.storeDirvalues loaded from global configuration orPNPM_CONFIG_STORE_DIRnow expand a leading~/to the user's home directory #6560.--shared-workspace-lockfilenow produces a warning when passed on the command line outside a workspace #1617.Windows
pnpm installon Windows now runs dependency build scripts from long global virtual store paths and normalizes scoped package paths in lifecycle scriptPATHentries #15111.pnpm installacross projects sharing a global virtual store on Windows no longer fails withAccess is denied, file-exists errors, or transient sharing violations #15114, #15176, #15171.pn,pnpx,pnx, andpnpmnow run when Git Bash, MSYS2, or Cygwin launches them through a Windows path #14884.pnpm dlxnow reuses cached packages when Windows creates directory junctions for its cache links #15171.pnpm pipeline --watchnow resolves Windows short paths so multiple path representations share the build cache #15105.CLI commands and output
pnpm removenow runs the project's ownpreuninstall,uninstall, andpostuninstallscripts #3276.pnpm remove -rnow fails before modifying manifests if any requested dependency is absent from all selected projects #2319.pnpm update --peernow updates ranges inpeerDependencies#8081.pnpm updatenow movesdevEngines.runtimeandengines.runtimeversion ranges to the resolved Node.js version #14988.pnpm update -gno longer reinstalls unchanged packages #12002.pnpm add -g,pnpm update -g, andpnpm remove -gnow recover a global package group whosenode_modulesdirectory was deleted #15093.pnpm add -gnow installs local tarballs whenPNPM_HOMEcontains..path segments #15118.pnpm versionnow readstagVersionPrefixfrompnpm-workspace.yaml, global config, orPNPM_CONFIG_TAG_VERSION_PREFIXwhen creating and reading Git tags #15044.pnpm publishnow allows a detached Git HEAD in CI environments #5894.pnpm store prunenow removes unreferenced files and packages from the content-addressable store #3635, as well as expired or supersededpnpm dlxcache data #15171.pnpm cache list-registriesnow prints decoded registry URLs #15046.pnpm deployno longer triggers an install when running scripts in a read-only deployed filesystem #11617.pnpm -r list --jsonnow outputs a single JSON array whensharedWorkspaceLockfileisfalse, and--longand--parseableread each project's own modules directory #15011.pnpm sbomnow 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 asUNLICENSED#14786.pnpm change checknow validates pending change intents in.changeset/#15183.pnpm --filterandpnpm -Fshell 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 runandpnpm run-scriptshell completion now suggests package scripts #15034.pnpm --versionno longer creates a temporary file in the project directory during store detection #15264.pnpm setupnow describes displayed configuration changes as "The following configuration changes were made" #15100.minimumReleaseAgeapproval prompts inpnpm installandpnpm update -gnow count and display each package version once #15083, #15091..npmrcauthentication 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-linkerishoisted#15161.pnpm installnow re-links a package's global virtual store slot afterallowBuildschanges #15117.Platinum Sponsors
Gold Sponsors
v12.5.1: pnpm 12.5.1Compare Source
Patch Changes
pnpm now reports an unknown task setting in
pnpm-workspace.yamland carries on. It used to refuse to start, so a project could not use a task setting that only the pnpm version itspackageManagerpins 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
registriesentries now route packages by exact names or trailing-prefix patterns inpackages. Registry declaration order no longer affects resolution. A matched package resolves exclusively from its assigned registry, including transitive and build dependencies. Usepackages: ["*"]to declare the default index.pnpm installno 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.tomllinked from a dotfiles repository.pnpm installnow returns "Already up to date" in a workspace wherededupeDirectDepsleft a project without anode_modulesdirectory of its own. Such a project forced a full install on every run.pnpm installno longer refuses the repeat-install fast path just because a changedpnpm-lock.yamlis 16 MiB or larger. Such a lockfile forced a full install on the run after every change.Platinum Sponsors
Gold Sponsors
v12.5.0: pnpm 12.5Compare Source
pnpm 12.5.0 makes Python a first-class ecosystem, accepts Package URLs in
pnpm add, names whole platforms insupportedArchitectures, and gives tasks machine-wide concurrency limits. It also fixes an install that could reuse one package's downloaded tarball for another.Minor Changes
Installing packages
pnpm addaccepts a Package URL in place of a package name.pnpm add pkg:npm/express@4.18.2savesexpresstopackage.json.pnpm add pkg:cargo/serde@1.0.188savesserdetoCargo.toml.pnpm add pkg:pypi/requests@2.31.0savesrequeststopyproject.toml.pkgis now a reserved specifier prefix, whatever case it is written in, so a named registry can no longer be calledpkg.A
registriesentry can now name the ecosystem it serves.ecosystemacceptsnpm,cargoandpypi. 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
registriesentry may not carry credentials. pnpm reads them from.npmrc, matched by origin, for a PyPI index as for every other package source.Configuring pnpm
supportedArchitecturesnow accepts a list of platforms, in place of theos,cpuandlibcaxes.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 inlinux-x64-muslorlinux-x64-manylinux_2_28. The Rust target triple of the same machine is accepted too, sox86_64-unknown-linux-gnunames the platformlinux-x64names. A Linux platform that names no C library is the glibc platform.currentis the platform the install runs on.The
os,cpuandlibcmapping keeps working and keeps its meaning.Added concurrency groups for tasks. A task in
pnpm-workspace.yamlcan name aconcurrencyGroup. The newconcurrencyGroupssetting 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 pipelineincluded. A task past the limit waits for a running one to finish. A script that callspnpm runfor a task of the same group runs under the slot its parent holds.toolsnames the programs pnpm downloads, andmirrorsays where each one comes from.node,bunandpythoncan be named. Any other tool is refused.mirroris the base a tool's own layout hangs off.channelssends one release channel elsewhere. A channel neither it nornode-mirror:<channel>names is left tomirror. Onlynodepublishes channels, so naming them for another tool is refused.Set it in the global
config.yamlor inPNPM_CONFIG_TOOLS. Apnpm-workspace.yamlthat names a tool mirror is ignored.pnpm pack-appdownloads the Node.js it embeds throughtools.node.node-mirror:<channel>keeps working and names the same thing as an entry underchannels.Python interpreters and environments
pnpm installnow 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 itsrequires-pythonaccepts, so a workspace can hold projects that support different Python versions. pnpm reads.python-versiontoo, and prefers the version it asks for. Setpython.executableinpnpm-workspace.yamlto name one interpreter for every project.pnpm installnow 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.runtimeOnFaildecides what an install with no interpreter that fits does, the way it does for a Node.js runtime.errorreports the project instead of installing one.warnandignoreinstall with an interpreter the machine has that the project'srequires-pythonrejects.tools.python.mirrornames a mirror.Python environments now live in the store. Each project keeps only its
.venvlink, which points at the project's current environment generation underpython-envsin the store. A repository with many Python projects no longer holds a.pnpm/python-envsdirectory in each of them. The next install relinks a.venvthat an earlier release published. The old.pnpm/python-envsdirectory is left in place, since a running program may still use it, and can be deleted once none does. WithfrozenStoreset, pnpm writes nothing to the store, so environments stay in the project's.pnpm/python-envs#15014.Python environments now use
packageImportMethodto import wheel files from the store. Useclone-or-copyfor copy-on-write clones with a copy fallback, orcopyfor 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.
Python projects and workspaces
pnpm installnow 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.packageoverrides that either way.pnpm installnow installs a Python project in the workspace from its own source. Declare it under[tool.uv.sources], asshared = { workspace = true }orshared = { 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
allowBuildsinpnpm-workspace.yamlas a Package URL, aspkg: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 installnow 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 = trueunder[tool.pnpm.python]in thepyproject.tomlthat declares[tool.uv.workspace].pnpm installthen resolves every member as one graph into onepylock.tomland one.venvat 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]inpyproject.toml#14945. Workspacepython.extrasandpython.groupsdefaults now skip names a project does not define.pnpm installnow reads dynamic Python project metadata from the build backend #14945. Projects with only arequirements.txtfile now get a Python environment and lockfile.Python dependencies and lockfiles
pnpm can now resolve
pylock.tomlfor several platforms and Python versions at once.supportedArchitecturesnames the platforms to lock for andpython.versionsthe versions. Every platform is paired with every version. One committed lockfile then serves Linux CI and macOS or Windows contributors #14945.The lockfile pins the wheel each environment takes for a distribution. It marks a package only some environments install.
pnpm installtakes 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 serverpnprServernames. Naming neither setting locks for the interpreter running the install.python.overridesandpython.constraintspin the versions a Python resolution may pick #14945. pnpm reads uv's own overrides and constraints frompyproject.tomltoo.pnpm installnow supports Python dependencies from Git repositories #14945. Direct wheel URLs are also supported. Sources can be declared in[tool.uv.sources]. Git dependencies requireallowBuildsapproval.pnpm installcan 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 inpylock.tomlby 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 withpkg:pypi/<distribution>: trueunderallowBuilds.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.
Patch Changes
Installing packages
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 installandpnpm addnow report an error whenpackage.json,pnpm-lock.yaml,pyproject.tomlor 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 --prodandpnpm install --devnow record every dependency group inpnpm-lock.yaml.node_modulesstill holds only the groups the filter selects. They used to write the filter into the lockfile, so a laterpnpm install --frozen-lockfilerejected it.pnpm prune --prod,pnpm prune --dev, andpnpm prune --no-optionalbehave the same way #14912.POSIX bin shims now convert a Windows-form path such as
C:\node_modules\.bin\tsccorrectly. The shim mangled the backslashes in such a path and could not reach the package it runs. Installing again replaces the shims already innode_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
resolvershook returns a resolution without one. Such a package installed alone, with none of its own dependencies and no warning #15000.pnpm installnow merges Git conflict markers inpnpm-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.Cargo projects
pnpm installcan now generateCargo.lockfor 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 installnow vendors recursive Git submodules for Cargo dependencies at their pinned commits. Cargo builds can use these sources offline. Set Git'sprotocol.file.allowtoalwaysto fetch local file submodules. pnpm fetches cached Git crates again on the first online install #14951.pnpm installnow generatesCargo.lockfor workspaces with Git dependencies, including a dependency that omits a package version. It also downloads the Rust standard library's dependencies when Cargo configuration enablesbuild-std#14944.pnpm installnow handles weak Cargo features, writtencrate?/feature. Resolution failed when one dependency turned on an optional crate and another asked for a weak feature of it #14960. The generatedCargo.locknow also includes the dependencies weak features reference, which Cargo rejected with--lockedfor crates such asuuid#14978.pnpm installnow generatesCargo.lockwhen 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 asno non-yanked version of napi-build satisfies ^3.0.0-beta#14952.pnpm installnow 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, <3span several of them #14962.Python projects
pnpm installnow 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 installnow installs wheels whoseRECORDhashes disagree with their contents. The wheel archive's locked SHA-256 hash remains verified. pnpm writes correct hashes to the installedRECORD#15061.pnpm installnow installs a Python wheel whoseWHEELfile lists tags that differ from the ones in its filename. A wheel whose filename tags were changed after the build, such asmysql-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 installno 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 nopyproject.tomlnow names the missing file and says where to run the command. It used to fail with a bareNo such file or directory (os error 2)#14945.Performance
pnpm auditno longer hangs on dependency graphs with many shared dependencies #15005.Sped up
pnpm installin 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 whenpnpm-lock.yamldiffers from the installed dependencies only by packages no project depends on or by top-level keys pnpm does not define.Other commands
pnpm deploynow links commands exposed by workspace dependencies into the deployed project'snode_modules/.bindirectory #14899.pnpm dlxandpnxnow 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-buildto allow the required builds.pnpm add -gandpnpm update -gnow 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 packnow 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 --longfills the Details column with the package homepage again #14886.Platinum Sponsors
Gold Sponsors
v12.4.2: pnpm 12.4.2Compare Source
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.
Patch Changes
Security
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
PATHfor 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.
Installing packages
pnpm no longer crashes at startup on FreeBSD and other Unix-like platforms. Platforms other than Windows and macOS use
~/.local/share/pnpm/storeby default #14859.pnpm installon Windows no longer fails withERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIRwhen clearingnode_modulescontaining linked dependencies, such as when changingnodeLinker#14790.pnpm install <pkg>now accepts--prodand--dev, including--prod=false#14868.pnpm installandpnpm updatenow honor--ignore-workspacein nested projects excluded from the surrounding workspace. The flag also skips that workspace's settings during thepackageManagercheck #14809.pnpm installon macOS no longer reuses stale files forfile:tarball or git-hosted tarball dependencies.pnpm installin a single-project directory now detectspackage.jsonedits made while the previous install was finishing #14890.pnpm install --frozen-lockfilenow removes packages no longer reachable from any project inpnpm-lock.yaml. This also prevents repeated lifecycle script execution and unnecessary installs beforepnpm runandpnpm execwithverifyDepsBeforeRun#14891.Resolving and updating dependencies
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 installnow rejects invalidpeerDependenciesspecifiers withERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION. A value such as"foo": "foo@1.0.0"previously created a broken directory link #14791.pnpm deploynow writes plain registry versions in the deployedpackage.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 apackage.jsonas@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
nextwithstyled-jsx's optionalbabel-plugin-macrospeer #14800.pnpm updatenow settles the lockfile in one run when an upgrade removes the package providing an optional peer dependency #14895.pnpm update --no-savenow preserves override-applied specifiers for dependencies it is not updating, preventing subsequent frozen installs from failing withERR_PNPM_OUTDATED_LOCKFILE#14836.pnpm update --no-savenow succeeds underminimumReleaseAgeStrictwhen every resolved version is old enough #14835.Performance
Workspace installs and
pnpm peers checkare 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.
Python projects
pnpm install --frozen-lockfilenow reusespylock.tomlacross compatible Python targets, including after kernel updates. Reuse requires unchanged requirements, index, andrequires-python, compatible wheels, and a locked dependency graph matching the target's markers #14843.The lockfile's
environmentsmarker 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-Pythonvalues, such as the trailing comma inopenpyxl3.0.x. pnpm treats these releases as declaring no interpreter range #14910.pnpm add pypi:...now rejects unsupported--save-prefixvalues before editing the manifest or resolving dependencies.Workspaces and scripts
Scripts listed in
syncInjectedDepsAfterScriptsno longer fail withERR_PNPM_INJECTED_DEPS_SYNC_READ_DIRwhen the lockfile contains an injected package copy that no project depends on.shellEmulatornow expands${VAR},${VAR:-default}, and${VAR:+alternative}in scripts #14814.Cargo and Python project discovery now honors
!exclusions inpnpm-workspace.yamlpackages, 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.
CLI and output
pn,pnpx, andpnxnow run the pnpm installed alongside them, even when that directory is absent fromPATHor another pnpm comes first #14803.pnpm --versionnow reports failures to install or record a project's pinned pnpm, then prints the running CLI's version. It also honors--store-dirand--store#14831.pnpm self-updateno longer reinstalls the active version when it was installed by the standalone installation script #14823.pnpm tandpnpm tstwork again as aliases forpnpm test.pnpm sbomnow emits valid repository URLs in CycloneDXexternalReferences[].urland SPDXhomepage. Shorthands such asvercel/msbecomegit+httpsURLs, embedded credentials are removed, and invalid repository values are omitted #14773.Platinum Sponsors
Gold Sponsors
v12.4.1: pnpm 12.4.1Compare Source
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.Patch Changes
Installing packages
pnpm installno longer fails withOperation not permittedwhen the filesystem refuses a hard link or a copy-on-write clone #14722. UnderpackageImportMethod: autoandclone-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 explicitpackageImportMethod: hardlinkorclonestill 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 underautoit stopped pnpm hard linking for the rest of the install.pnpm installno 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 installandpnpm dlxon 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 installno longer fails with "Invalid cross-device link" while preserving a package's nestednode_modulesdirectory during a Docker build #14758.pnpm installno longer fails on a package tarball that carries a file at the archive root, such as the._*entries macOStaradds #14701. The file is installed at the root of the package.A
file:tarball packed without the usualpackage/directory is now recorded under the name and version from its ownpackage.json. It was recorded under the alias the dependency was given, at version 0.0.0.Under
nodeLinker: hoisted,pnpm installno longer re-imports packages that are already in place. A repeat install replaced the wholenode_modulestree and reportedPackages: +N. A package is still imported when its directory is missing, when itspackage.jsonno longer carries the installed version, when it is afile:dependency, and when it is patched. Lifecycle scripts no longer run again for a package left in place, andpnpm rebuildand a change toallowBuildsstill reach it.pnpm installnow 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.Resolving and linking dependencies
pnpm install,pnpm add, andpnpm dedupenow applyignoredOptionalDependencies#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 installno longer links a transitive dependency to a workspace package whenlinkWorkspacePackagesistrueand the dependency is declared with a plain version range #14781. EnablingpreferWorkspacePackagesdoes not change this. SetlinkWorkspacePackages: deepto link them.pnpm installno longer leaves dangling dependency links in workspace packages located above the workspace root #14726.pnpm installandpnpm addno longer leave a dangling symlink innode_moduleswhen a project starts depending directly on a package that the lockfile holds only as a transitive dependency with resolved peer dependencies #14714.pnpm dedupenow 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 checkno longer reports a peer dependency declared asworkspace:^,workspace:~, or a bareworkspace:as unmet #14770. pnpm reported these as unmet whatever version the linked workspace project supplied.Performance
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 innode_modules/.binmakes 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 installin 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.
Running scripts and tasks
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-bailnow lets every matched script finish after one of them fails #14718. The command exits withERR_PNPM_RUN_FAILED, and its message lists the scripts that failed in the order they were selected.pnpm pipelineno longer fails on a project that tracks a symlink, such as aCLAUDE.mdpointing atAGENTS.md#14692. Changing a symlinked input's target invalidates that task's cache, andpnpm pipeline --no-cacheno longer hashes task inputs.Commands
pnpm add -g,pnpm update -g, andpnpm remove -gno 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 dedupenow 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-matchexits with an error when no project matches.pnpm update <name>@<version>now keeps the range operator the manifest declares #14745. Runningpnpm update react@19.3.0on"react": "^19.2.8"writes"react": "^19.3.0". Ajsr:entry keeps itsjsr:prefix, and a plainpnpm updatenow moves ajsr:range the way it moves an npm range.pnpm --filterdirectory 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 --legacynow prefers the dependency versions pinned in the source workspace lockfile when they still satisfy the deployed project's range #13857.pnpm sbomnow 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 noauthorfield inherits the workspace root's author.pnpm sbom --sbom-format spdxnow writescreationInfo.createdwith whole seconds, such as2026-09-08T10:38:21Z#14684. The fractional seconds it carried were rejected by strict SPDX consumers.Configuration
The
updateConfigpnpmfile hook now receives the resolved configuration, including settings that came from.npmrc, the command line, or a default #14676. Scoped registries are reported underregistriesByScope, and a hook may rewrite that map to change where packages are fetched from. Registry credentials are reported underconfigByUri, as pnpm 11 reports them. An unset setting is left out rather than reported asnull.pnpm audit --fixand theminimumReleaseAgeStrictapproval prompt now keep the comments inminimumReleaseAgeExcludewhen they append an entry to it inpnpm-workspace.yaml. The rest of the list is left as written, and thetrustPolicyExcludePruneandminimumReleaseAgeExcludePrunecleanups keep the comments of the entries they retain.pnpm installandpnpm dedupenow run those cleanups too #14759. Onlypnpm add,pnpm update, andpnpm removepruned the entries that the freshly written lockfile no longer resolves.pnpm config set --global node-download-mirrorsno longer rejects the key #13611. The global config file already acceptednodeDownloadMirrors, but the command refused to write it.NO_PROXYentries 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.yamlwhendevEngines.packageManager.onFailisdownloadand lockfile writing is off throughlockfile: falseor--no-lockfile#14728. pnpm still switches to the pinned version.pnpm now writes
node_modules/.package-map.jsononly whennodeExperimentalPackageMapis enabled. Nothing reads the file without that setting, and an install that stops writing the map removes the one a previous install left.Windows
pnpm pipelineno 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.
Messages and output
pnpm now warns when the root
package.jsondeclares a non-emptyworkspacesarray and the project has nopnpm-workspace.yaml#2255. Such an install linked no project and said nothing about why.ERR_PNPM_PACKAGE_MANAGER_REMOVE_MODULES_DIRnow names the file or directory innode_modulesthat pnpm could not clean up. It reported only the underlying OS error, such as "Access is denied (os error 5)".pnpm --helpno longer describes pnpm as experimental.Platinum Sponsors
Gold Sponsors
v12.4.0: pnpm 12.4Compare Source
Minor Changes
pnpm can now manage npm, Python, and Cargo dependencies in the same workspace. Enable
python.enabledorcargo.enabledinpnpm-workspace.yaml, then usepnpm installto install them together.pnpm add pypi:<package>. pnpm usespyproject.toml,pylock.toml, and a managed.venv. Frozen and offline installs are supported, andpnpm runandpnpm execmake the environment's executables available #14566.pnpm add crate:<package>. pnpm supports crates.io and custom sparse registries configured withcargo.indexUrl. Registry authentication supports pnpm credentials and, for crates.io,CARGO_REGISTRY_TOKENor$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 inpipelines. It selects affected projects, runs their task graph, and continues running tasks after a task fails.Tasks support
inputs,outputs,env, andcachesettings. Cached results restore task outputs and replay logs. Cargo tasks can reuse local build state between worktrees withtasks.<name>.cargoTargetDir. SetincludeWorkspaceRoot: trueto include root tasks.Use
pnpm pipeline --dry-runto 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
trustPolicyExcludePruneto automatically remove unused versions and packages fromtrustPolicyExcludewhen runningpnpm add,pnpm update, orpnpm remove. It is disabled by default. Package name patterns such as@scope/*are kept, and cleanup is skipped whensharedWorkspaceLockfileisfalse.Added
pnpm change checkfor CI validation of package versions against theversioning.epicsbands andversioning.fixedgroups inpnpm-workspace.yaml. It reports all violations, including packages that are not part of the current release.Patch Changes
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 viewnow shows full registry URLs. Scripts that parse the directory names frompnpm cache list-registriesorpnpm cache listneed updating.Patches that add build scripts or a
binding.gypnow 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>andpnpm 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
.npmrcnow takes precedence over registry settings saved bypnpm loginin the globalconfig.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-timeoutnow 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 deployis faster in large workspaces and no longer fails withERR_PNPM_LOCKFILE_CONFIG_MISMATCHwhen the project includes a.pnpmfile.mjs#14539, #14671.pnpm add --workspace <pkg>works again. It saves the dependency with theworkspace:protocol and links it from the workspace. The command fails if no workspace project provides the package #14602.pnpm addandpnpm installnow accept protocol-prefixed selectors such asjsr:@scope/pkg,npm:pkg@^1.0.0, andworkspace:pkg@*#14590. Installs with JSR dependencies in the lockfile also no longer fail withERR_PNPM_META_FETCH_FAIL#14649.Boolean flags now accept explicit inline values. For example,
pnpm install --prod=falseinstalls devDependencies, while--prod=trueskips them #14553.pnpm install <pkg>now accepts--offlineand--prefer-offline, aspnpm add <pkg>already did #14194.Fixed
pnpm install --frozen-lockfilerejecting a freshly generated lockfile when overrides use relativefile:orlink: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
lockfileDiris set. The pins also remain consistent across commands when version switching is disabled, avoiding unnecessary lockfile changes #14633, #14575.pnpm importnow respectslockfileDirand branch lockfiles without modifying other lockfiles. Failed imports restore the destination lockfile #14563.pnpm patch-commitnow produces valid patches when files are added or deleted.pnpm installalso 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,
<=16now includes all 16.x versions, and>=0.11 <=3correctly accepts 3.0.1 #14419.Workspace package patterns now support
.and..segments and repeated slashes. Patterns such as./packages/*and exclusions such as!./packages/foonow match correctly #14571.packageConfigssettings now apply to the specified projects whensharedWorkspaceLockfileisfalse, includingoverrides,hoist,modulesDir,saveExact, andsavePrefix. Workspaces with a shared lockfile report which entries were ignored #14556.pnpm runandpnpm execno longer report a changed workspace structure after a successful install whensharedWorkspaceLockfileisfalseandverifyDepsBeforeRunis enabled #14588.Commands run from a project's subdirectory now find the nearest ancestor with a manifest. This fixes commands such as
pnpm binreturning paths under the wrong directory.pnpm initstill creates its manifest in the current directory, andpnpm execstill runs there #14622.Relative
scriptShellpaths inpnpm-workspace.yamlnow resolve from the workspace root, including when scripts run in nested packages. Bare command names such asbashstill usePATH#14422.Fixed installing the pnpm version pinned in
packageManagerwhennodeLinkerishoisted. Managed Node.js, Deno, and Bun installations also work when the global config usesnodeLinker: 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_TOKENorGITHUB_TOKENwhen available to avoid GitHub's anonymous API rate limit in CI. Tokens are only sent whenstrict-sslis enabled.Fixed concurrent installs sharing a global virtual store on macOS failing with "failed to import ... No such file or directory" #14560.
Fixed
pnpm setupfailing withERR_PNPM_DIRECTORY_FETCHER_PATH_ESCAPEon Windows. Localfile: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
shellEmulatorenabled. Paths ending in a backslash, line breaks, and literal shell expressions are preserved #14548.Windows store paths now consistently use backslashes in
pnpm store pathoutput and in thestoreDirandvirtualStoreDirfields ofnode_modules/.modules.yaml.Invalid certificates in
caorcafileno longer cause anInvalid CA certificateerror. Valid certificates still apply, and blankcertorkeyvalues are treated as unset #14646.Installs now respect the archive extraction concurrency limit even after a download is abandoned #14585.
pnpm auditsummaries now exclude advisories ignored throughauditConfig.ignoreGhsasand report them separately. When all advisories are ignored, the summary says so #14535.pnpm pack --jsonnow reports errors as JSON. Lifecycle script output appears before the final JSON output.pnpm outdated -rnow wraps theDependentscolumn, keeping the table readable when many workspace projects use the same dependency #14591.Shell completions now support the
pnalias in bash, fish, pwsh, and zsh #11955.pnpm versionnow accepts-mas a short alias for--message#14567.Platinum Sponsors
Gold Sponsors
v12.3.4: pnpm 12.3.4Compare Source
Patch Changes
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, andpnpm install --dangerously-allow-all-builds. pnpm 12 rejected them withunexpected argument, which failed every install on Vercel, whose build runspnpm install --unsafe-perm#14346.pnpm removenow accepts--unsafe-perm, the same flagpnpm install,pnpm add, andpnpm updatetake.Platinum Sponsors
Gold Sponsors
v12.3.3: pnpm 12.3.3Compare Source
Patch Changes
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
packageManagerprovisioning, Bun, Deno, ornpm install --ignore-scripts. Those installs previously failed withsyntax error near unexpected token ')'. They still cannot run pnpm on Windows. On macOS only a shell can start it #14346.Platinum Sponsors
Gold Sponsors
v12.3.2: pnpm 12.3.2Compare Source
Patch Changes
pnpm audit --fix updateno 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 installno 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 runandpnpm execnow 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 importnow keeps the versions recorded inpackage-lock.json,npm-shrinkwrap.json, oryarn.lockwhen it generatespnpm-lock.yaml. A range inpackage.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 importin a workspace now imports every workspace project into the shared lockfile. It previously imported only the project in the current directory.pnpm importnow fails withERR_PNPM_LOCKFILE_NOT_FOUNDwhen none of the three source lockfiles is present. It also fails withERR_PNPM_YARN_LOCKFILE_PARSE_FAILEDwhen it cannot parseyarn.lock. It previously generated a lockfile from scratch in both cases.pnpm importalways resolves locally. It warns when--pnpr-serveror thepnpr-serversetting 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 andregistry.npmjs.orgis unavailable #14483.Sped up installs in large workspaces. Saving the lockfile is faster, and the install finishes without waiting for memory cleanup #14352.
pnpm installnow relinks workspace packages whenpublishConfig.linkDirectorychanges. 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 installnow reports "Already up to date" when local tarball dependencies have not changed #14495.pnpm updatenow accepts--ignore-scriptsand skips lifecycle scripts during the update pnpm/pnpm#14512.Sped up installs that restore a deleted
node_modulesfrom a warm global virtual store. pnpm no longer re-links packages that are already fully present in the global virtual store #14510.Platinum Sponsors
Gold Sponsors
v12.3.1: pnpm 12.3.1Compare Source
Patch Changes
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, andyarnfailed withunexpected 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.
Platinum Sponsors
Gold Sponsors
v12.3.0: pnpm 12.3Compare Source
Minor Changes
Every context-aware global command (
node,deno,bun, and the shims created withpnpm 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>.exereplaces the.cmdand.ps1shims for them. Shims written by earlier pnpm 12 releases are migrated on the next global install or self-update.pnpm removeandpnpm updatenow accept--trust-lockfile,--no-trust-lockfile,--trust-policy,--trust-policy-excludeand--trust-policy-ignore-after, the same flagspnpm installandpnpm addtake, so the supply-chain settings can be overridden for a single run.pnpm removeverifies the lockfile against the active policies the waypnpm installdoes, and--trust-lockfileskips that pass for every entry, not only the package being removed.pnpmnow also honors--config.trust-lockfile=<value>, and accepts the bare--trust-lockfile/--no-trust-lockfilespelling on the commands that previously took the setting from the config file alone.Patch Changes
pnpm add <local directory>,pnpm add <local tarball>,pnpm add file:<path>andpnpm add <tarball URL>work again. A specifier given without a<name>@prefix is no longer read as a registry package name and rejected withERR_PNPM_PACKAGE_MANAGER_ADD_RESOLVE_LATEST#14437.Fixed
pnpm deploy --legacyignoringallowUnusedPatchessupplied through--config.allow-unused-patchesor thePNPM_CONFIG_ALLOW_UNUSED_PATCHESenvironment variable pnpm/pnpm#14450.Fixed
pnpm install --lockfile-onlywriting a lockfile that referenced a missing peer-suffixed snapshot when an npm-aliased dependency took part in a cyclic peer dependency graph. The followingpnpm install --frozen-lockfilefailed withERR_PNPM_LOCKFILE_MISSING_DEPENDENCY#14449.pnpm confignow accepts-g/--global,--location, and--jsonbefore its subcommand pnpm/pnpm#14421.pnpm dedupenow 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
pnpmdirectly, without a shell, asnrfrom@antfu/nidoes #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.yamlis now read while the workspace projects are being discovered #14352.Fixed filtered and recursive
pnpm runandpnpm exechanging 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 declaringconcurrency: 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 aspnpm --workspace-root with current --versionpnpm/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 echoandpnpm -z exec echofail the waypnpm --tag next exec echodoes.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 outdatedandpnpm updatenow 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 --helpdescriptions of--prodand--devno longer claim that the flags take precedence overNODE_ENV. pnpm does not readNODE_ENVwhen 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.confcontaining an option the bundled pure-Rust resolver did not recognize, such asoptions 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.
catalogModeand--save-catalogno longer move a local path, tarball, orworkspace:<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.yamlin large workspaces #14352.Fixed non-frozen installs through a pnpr server failing instead of regenerating a conflicted lockfile.
pnpm update --interactiverenders 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.atoggles all andiinverts the selection. The confirmed selection is echoed as a list of package names #14423.Fixed
pnpm configcommands 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
.npmrcvalues, 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 orpnpm add <url>cannot leak them into terminal scrollback or CI logs.When
dist-tags.latestnames 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 aspnpm testnow load the pnpmfile, soupdateConfighook settings such asextraEnvandextraBinPathsreach the scripts they spawn #14433.The
pnpmexecutable of the npm package now works when the package was installed without running its install scripts, as under--ignore-scriptsor 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.
minimumReleaseAgeStrictnow defaults totruewhenminimumReleaseAgeis explicitly configured, whether inpnpm-workspace.yaml, the globalconfig.yaml, aPNPM_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 tominimumReleaseAgeExcludeinstead 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 --globalon Unix pnpm/pnpm#14417.Fixed
pnpm repoandpnpm docsfailing to open the Windows browser from WSL pnpm/pnpm#14467.pnpm link,pnpm outdated, andpnpm importnow apply pnpmfileupdateConfighooks before resolving dependencies.Fixed standalone installations to preserve the bundled
node-gypfiles used to build native dependencies.Fixed resolution against registries whose version manifests carry
_npmUser,dist.attestations,dist.unpackedSize,dist.fileCount, orpeerDependenciesMetain a shape npm does not use. Such a version was skipped as though it had never been published, sopnpm addcould fail with "no version found for the latest tag" even though the registry served it.pnpm unpublishnow completes the two-factor authentication a registry asks for instead of failing withERR_PNPM_UNAUTHORIZEDwhile 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.exeagain after everypnpm self-update#14405.Platinum Sponsors
Gold Sponsors
v12.2.1: pnpm 12.2.1Compare Source
Patch Changes
pnpmexecutable target without a file extension so pnpm 12.1 and earlier can upgrade to newer pnpm 12 releases on POSIX systems.Platinum Sponsors
Gold Sponsors
v12.2.0: pnpm 12.2Compare Source
Minor Changes
workspace:protocol.Patch Changes
Fixed
pnpm audit --fixfailing withERR_PNPM_INVALID_FIX_OPTIONwhen used without a value, including when another flag follows it, as inpnpm audit --fix --json#13261. Fixedpnpm audit --fix=overrideignoring thesaveExactandsavePrefixsettings when writing vulnerability overrides #11523.Authenticate Node.js runtime downloads from
nodeDownloadMirrorswith URL-scoped npm registry credentials, including bearer tokens, basic auth, andtokenHelperpnpm/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-lockfileto derive its repair and filtered-merge views from one lockfile snapshot.Load pnpmfile
updateConfighooks before packing so hook-provided catalogs resolve inpnpm pack,pnpm publish, andpnpm stage publishpnpm/pnpm#14377.pnpm deployno longer requiresinjectWorkspacePackagesto be enabled. A linked workspace dependency is rewritten to afile: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 anoverridesentry 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_CHMODwhen 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 pnpmon Windows so it invokes the nativepnpm.exebinary pnpm/pnpm#14362.Fixed context-aware global shims on WSL2 so native Linux installations dispatch through the project runtime.
pnpm installno longer writes globalminimumReleaseAgeExcludeentries to the project'spnpm-workspace.yamlpnpm/pnpm#14347.Fixed
catalog:ranges in workspace package peer dependencies being reported as unmet pnpm/pnpm#14361.globalDirandglobalBinDirare honored wherever they are set, sopnpm add -gno longer fails withERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATHafterpnpm config set -g global-bin-dir#14336. The globalconfig.yamlis read again,PNPM_CONFIG_GLOBAL_DIR/PNPM_CONFIG_GLOBAL_BIN_DIRreach the directories derived from them, and a leading~/is expanded before that derivation. A project'spnpm-workspace.yamlstill cannot set either key.Fixed the install progress line reporting
added 0undernodeLinker: hoisted, even when packages were linked intonode_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 toworkspaceConcurrency, 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**skippackages/.cacheand.git. A pattern that names a dot-prefixed directory still matches it, aspackages/.cacheandpackages/.*do.pnpm auditnow ends its output with a trailing newline, including the--json,--fix, and--ignoreoutput.Retry transient Windows file-lock errors while replacing hoisted packages during installation.
This fixes pnpm/pnpm#14349.
Fixed command-line
--side-effects-cacheoverrides being ignored whenpnpm-workspace.yamluses the object form ofsideEffectsCachepnpm/pnpm#14338.Speed up workspace project discovery in large monorepos: workspace patterns are now probed concurrently and the discovered projects'
package.jsonfiles are read in parallel #14352.Fixed repeated
pnpm deduperuns alternating between peer resolutions when a peer is provided through an npm alias.Fixed
pnpm repo <package>andpnpm docs <package>resolving bare package names through thelatesttag, 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é-storeare now preserved #14383.Platinum Sponsors
Gold Sponsors
v12.1.0: pnpm 12.1Compare Source
Minor Changes
pnpm loginandpnpm addusernow record the granted token in the globalconfig.yaml, under the_authsetting, with--scope's scope routed to that registry underregistries.pnpm logoutremoves it from there, and still from anauth.inian earlier version wrote. Tokens already inauth.inikeep working.A
scopeset in a project'spnpm-workspace.yamlis now ignored, with a warning naming where to set it instead.pnpm loginrecords the scope as a@scope:registryroute in the machine-globalauth.ini, which outranks~/.npmrcin every project — so a repository-committed file could redirect a scope such as@acmefor all of a user's other projects after one routine login. Use--scope, thePNPM_CONFIG_SCOPEenvironment 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-fromskips exactly the work that passed during a matching interrupted or failedpnpm -r run/pnpm -r execinvocation. 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>.concurrencyinpnpm-workspace.yamlto limit how many instances of that task may run across workspace projects at once:sideEffectsCachenow 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: true,sideEffectsCacheReadonly,remoteSideEffectsCache, and itsorganizationfield 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: truenow blocks writing to the cache, and setting it alongsidesideEffectsCache: falsegives 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 runandpnpm -r execnow 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
taskssection inpnpm-workspace.yamldeclares what a task depends on, using the^convention:^namemeans the named task in each of the project's workspace dependencies; a barenamemeans the task in the same project; an entry with nodependsOndeclares an empty dependency list. A task with no entry behaves asdependsOn: ['^<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. SettingignoreWorkspaceCycles: truedowngrades the error to a warning: the cycle's tasks run in an arbitrary order relative to each other.--resume-fromnow 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 theverifyDepsBeforeRuncheck);--jsonemits 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.
Patch Changes
An
_authentry in the global config file no longer decides which registry packages come from when something else says. Aregistryorregistriesdeclared inpnpm-workspace.yamlor the global config now wins over the route inferred from a stored credential, which still applies where nothing else declares one. Thepnpm_config__authenvironment 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 --legacyto exclude dependencies that are only reachable from unselected workspace projects afterpnpm fetch.Fixed dependency-verification install logs corrupting
pnpm execoutput and ignoring--silentpnpm/pnpm#14197.pnpm clean/pnpm purgerun from a workspace subdirectory now remove each project's ownnode_modulesinstead of emptying the workspace root's for every project #14239. A custommodulesDiris resolved against each project directory too.pnpm dlx <pkg>@catalog:now resolves the specifier through the calling workspace's catalogs instead of failing withERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC#14294.Fixed
pnpm doctorreporting a version that does not matchpnpm --versionpnpm/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 --versionprobe 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
.mjspnpmfile 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 --prodfailing when an excluded dev dependency was also declared as an optional peer dependency pnpm/pnpm#14302.Fixed
pnpm packto respect thefilesfield when deciding whether to include root-level changelog, history, and notice files.pnpm update -gno longer downgrades a global package.--latestresolves thelatestdist-tag, which can point at an older release than the one installed — afterpnpm add -g <pkg>@next, for instance #14270.pnpm update -galso no longer changes the pnpm version. pnpm's own global install belongs topnpm 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 updateno longer replaces the specifier a project declares for a dependency that is also listed inoverrides. Acatalog:reference stays acatalog:reference, and a declared range stays as written, instead of being rewritten to the version the override resolved to #12115.pnpm updateno longer moves the range a project declares for a dependency thatoverridesalso lists, even when the override repeats that range verbatim. Previously the updatedpackage.jsondisagreed with the lockfile, so the nextpnpm install --frozen-lockfilefailed with a specifier mismatch #14224.Allowed pnpm's shared-artifact client to connect to an artifact-only pnpr tier.
Rebuilding
node_modulesfrom an up-to-date lockfile is up to ~200 ms faster: thenode --versionprobe 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
--jsonis passed topnpm viewor its aliases (info,show, andv).Installs complete faster on workspaces with many projects: each project's
node_modulesis now linked concurrently.Fixed
patchedDependenciesmatching for git-hosted dependencies during fresh and frozen installs pnpm/pnpm#14273.pnpm pm <command>works again: thepmprefix, which forces pnpm's built-in command over apackage.jsonscript of the same name, is recognized instead of failing withERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL/Command "pm" not found.pnpm pm cleanandpnpm pm purgenow removenode_moduleseven when the project (or the workspace root) declares aclean/purgescript #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=valueor--setting value, and overrides the same setting read frompnpm-workspace.yamlor.npmrc#14281.pnpm add,pnpm update, andpnpm removenow savepackage.jsonbefore failing withERR_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 deniedfailures when concurrent global commands hand off the global bin lock on Windows.Fixed the
--shamefully-hoistCLI 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_KEYSand..._PUBLISH. ThePNPM_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.packageManagerrange pin on pnpm is now recorded inpnpm-lock.yaml'spackageManagerDependencieswhen 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 thanpnpm 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_modulesto rediscover bins that were already linked.Fixed a large install-time regression on macOS for installs that rebuild
node_modulesfrom 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 installnow materializes each package once into the store'slinksdirectory (the same canonical slotsenableGlobalVirtualStoreuses) and copies it intonode_modules/.pnpmwith a single copy-on-write directory clone per package, replacing tens of thousands of per-file syscalls with one per package. Applies with the defaultnodeLinker: isolatedwhenenableGlobalVirtualStoreis off andpackageImportMethodisauto,clone, orclone-or-copy; hoisted, global-virtual-store, and explicithardlink/copyinstalls are unchanged.Stop in-flight recursive
runandexeccommands when bailing after the first failure.Warm installs that rebuild
node_moduleson macOS are about 10% faster: creating each package's virtual-store directory now issues fewer filesystem calls.An
_authcredential in an.npmrcnow 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_auththat is not valid base64, or that carries no:between the username and the password, now fails withERR_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 listnow style their output there.Platinum Sponsors
Gold Sponsors
v12.0.0: pnpm 12Compare Source
Major Changes
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 agitclone 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:
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.yamlmay no longer carry a setting pnpm does not recognize. Such a setting used to be ignored in silence — a misspelledminimumReleaseAgedropped 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 withERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGSwhen 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 configsubcommands never fail on such a setting, so a broken file can still be inspected and repaired, andpnpm 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 topnpm-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: autonow 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 materializingnode_modulesfrom a warm store. ext4 installs are unchanged (cloning was never supported there, soautoalready hardlinked), and macOS keeps clone-first, where APFSclonefileis the platform's cheap primitive. Cloning remains the fallback when the store refuses hardlinks, and remains available explicitly viapackageImportMethod: clone.Under
engineStrict, an install fails when an incompatible package is reached through a regulardependenciesedge of an installable package, even when that whole subtree hangs off anoptionalDependenciesentry. 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.pnpm install --frozen-lockfile falseis no longer supported. Usepnpm install --no-frozen-lockfileto disable frozen-lockfile mode. To enable it, usepnpm install --frozen-lockfilewithout a separatetrueargument #14741.Minor Changes
Globally installed bins can now follow the project you run them in. The new
globalShimssetting 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, soglobalShims: { bun: false }switches one default off andglobalShims: { typescript: true }adds another package. With the default, a project that pins Node.js throughdevEngines.runtimeorengines.runtimegets the pinned stable release — authenticated against the Node.js release-team signatures — downloaded on first use and run whenever you typenodeinside 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 shorthandtrue) defers to artifact authentication,"always"switches without ever asking (useful in CI), and"prompt"always asks, even for authenticated candidates. SetglobalShims: falseto disable the feature, orPNPM_SHIM_BYPASS=1to bypass it for one invocation. On Windows, programs can keep spawning the globalnode.exedirectly, 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.packageManagerpin is honored, and ayarn.lockwritten 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@4was previously a missing version, since Yarn 4 is published as@yarnpkg/cli-dist, andpnx node@22now runs that Node.js release rather than a wrapper that downloads one.--packagenaming a package manager picks which of its commands to run, sopnx --package npm@11 npx create-somethingruns that npm'snpx.pnpm shim add yarnlinks ayarncommand that runs whatever version the current project pins, andpnpm shim rm/pnpm shim lsmanage those shims. It works for any package, not only package managers. Shims are never created as a side effect ofpnpm setupor an install — a shim shadows the rest of yourPATH, 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 followsdevEngines.runtime: the pinned version runs where a project pins one, and the globally installed copy is the fallback everywhere else. An explicitglobalShimsentry, includingfalse, is left as you set it.pnpm addfollows the same rule about what a name means.pnpm add -g yarn@4installs Yarn Berry — it used to fail, because npm'syarnpackage stops at Classic — andpnpm add -g node@22/pnpm add -g deno@2install 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 underengines.runtimeasnode@runtime:22already did.The declaration goes where the package manager reads it. Yarn is started from a project pin by corepack, which reads only
packageManagerand only accepts an exact version there, sopnpm add yarn@4resolves the line and writes"packageManager": "yarn@4.18.0"— the same thingcorepack use yarn@4writes, down to the+sha512.…integrity for the Yarn Classic line that corepack pins its tarball with. Every other package manager is recorded indevEngines.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 yarnrecords the project's package manager instead of installing the npm package that shares the name (that package is still reachable aspnpm add yarn@npm:yarn@1.22.22),pnpm add -g yarninstalls the current Yarn line rather than Classic,pnpm add -g node/pnpm add -g denoandpnx node/pnx denoinstall 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
remoteSideEffectsCachesetting. A workspace names the eligibleorganizationandpackages; everything describing the act of signing —publish,keyId,builderId,trustedKeys,privateKeyand the provenance fields — is refused inpnpm-workspace.yamland read from the global config file or the environment instead.Added the
audit.ignorePrunesetting. When set totrue,pnpm audit --fixremoves ignored GHSA entries that no longer appear in the audit report.pnpm initnow 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 owndevEngines.packageManager/packageManagerpin #7490.The version is read from the
latesttag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry,offline, or alatestthat theminimumReleaseAge/trustPolicysettings reject —pnpm initpins the running version as before, and never fails or hangs on the lookup. Alatestthat is older than the running pnpm is never pinned either.Allowed
pnpm update --patchesto refresh registry revisions through a configured pnpr server while retaining locked package versions.Added explicit registry revision selection with
<version>+rNandpnpm update --patchesfor 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
registriessetting, 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 aspnpm add --global) throughsudonow fails withERR_PNPM_SUDO_NOT_SUPPORTEDinstead 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 aspnpm bin --global) still work under sudo.pnpm stage approvenow 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.Patch Changes
Deprecated the pnpmfile
filterLoghook 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/typesgained atypescriptdependency resolved to the newest release, which put TypeScript 7 under older@typescript-eslintversions and made ESLint fail with "Cannot read properties of undefined (reading 'Intrinsic')". The database keeps its@yarnpkg/extensionsentries 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-storeinstead 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.Platinum Sponsors
Gold Sponsors
v11.28.1: pnpm 11.28.1Compare Source
pnpm 11.28.1 makes
pnpm installwork in StackBlitz WebContainers, repairs lockfiles whose patch hashes disagree withpatchedDependencies, and fixes many bugs in Windows command shims, injected workspace dependencies, andpnpm deploy.Patch Changes
Installing packages
pnpm installnow works in StackBlitz WebContainers. On projects without a lockfile, it used to fail withENOENT ... pnpm-lock.yaml, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls. It also failed with "this.db.exec is not a function", becausenode:sqlitethere lacksDatabaseSync.exec. Whennode:sqlitecannot prepare statements either, pnpm stores the index inindex.fallback#15649.pnpm installnow completes after downloading a Node.js runtime specified bydevEngines.runtimewhen pnpm runs on Node.js 24.4.x #14667.pnpm installno longer fails when a package from the registry declares afile: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 aslink:<root>/typings/css-tree#9141.Installing a git-hosted dependency that has to be built no longer fails when that dependency's own dependencies have build scripts nobody approved. pnpm skips those builds while preparing the dependency, as it does without
strictDepBuilds#9764.pnpm now uses pnpm to prepare a git-hosted dependency that is a pnpm workspace without a committed lockfile. It used npm before, which could skip the dependency's build #14011.
When installing a git dependency over SSH fails with
Permission denied (publickey), pnpm suggests checking the loaded keys withssh-add -l. Resolving an SSH URL that refuses the key also shows a local HTTPS rewrite that leaves the recorded URL alone #13743.pnpm install --devandpnpm fetch --devnow install the optional dependencies of devDependencies, such as the platform binaries of Biome and oxlint. The project's ownoptionalDependenciesare still skipped #9678.pnpm install --frozen-lockfilenow works on a detached HEAD whengitBranchLockfileis enabled. The install now reads the lockfiles of the local and remote-tracking branches that contain the checked-out commit. It still writes the sharedpnpm-lock.yaml#7672.pnpm installon CI now fails on an outdated lockfile whenpreferFrozenLockfileis explicitly set totrue. Setting it totrueused to let CI update the lockfile #9072.pnpm installnow fails withERR_PNPM_IGNORED_BUILDSon a repeat install whenstrictDepBuildsis on and a dependency's build is still undecided. A repeat install against an existingnode_modulesreported success where a fresh install failed #10450.pnpm installnow removes an optional dependency fromnode_modulesif its install script fails. Code that checks whether the package is installed no longer finds a package that cannot load #8756.pnpm install --offlineandpnpm add --offlinenow 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 withERR_PNPM_NO_OFFLINE_TARBALLwhen its tarball was missing #10715.If an offline install fails because the registry metadata cache uses the layout from before pnpm 11.27 and 12.4, the error now names the older mirror on disk and explains that one online install repopulates the cache #15656.
pnpm installnow fails right away when writing package files fails because the store is full. It no longer retries the tarball download first #8581.With
nodeLinker: hoisted,pnpm installnow restores a workspace project'snode_modulesafter it was deleted. Before, the install printed "Already up to date" and left the project without the dependencies nested under it.Under
nodeLinker: hoisted,pnpm installnow clears orphaned package directories that an interrupted or failed install leaves in a project'snode_modules. A directory recorded by the previous install is removed, while an unrecorded directory is moved tonode_modules/.ignored. A copy already in.ignoredis never overwritten #13676.Packages in an external
virtualStoreDircan resolve the project's direct dependencies selected byhoistPattern. Runpnpm install --forceto repair an existing installation #5652.A repeat install now keeps the fast path when a declared local file dependency is replaced by an override #12892.
Store, build cache, and global virtual store
Files imported from the store now follow the umask of the install that writes them. Installing with a umask of
077no longer leaves imported files readable by the group and others #3807.With the global virtual store,
pnpm rebuildno longer modifies packages shared with projects that have not approved their build scripts #12302.The side-effects cache now restores the symlinks that a build script creates inside a package. A warm install used to replace each of them with a copy of its target #12859.
After upgrading, every package with a build script is built once more.
pnpm installnow restores cached build artifacts when reinstalling a workspace that uses separate lockfiles #12942.The global virtual store and the side-effects cache now key built packages by the Node.js version that the root project's
devEngines.runtimeorengines.runtimepins. That is the Node.js their build scripts run with. A dependency that declares its ownengines.runtimeno longer changes the key for every other package.Concurrent installs that share a global virtual store now run a package's build in its shared slot one at a time. A failed build leaves the slot in place and marks it for the next install to rebuild #15568.
Concurrent installs no longer fail when they replace the same stale hoisted dependency link. Virtual store cleanup now keeps the temporary lockfiles that concurrent installs are still writing.
pnpm installkeeps the owner, group, and mode of files already in a shared store, includingindex.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 installrepairs a store file that was modified through a hard link innode_modules, the repair now keeps the file's inode on Linux and macOS. Hard-linked copies in other projects are healed at the same time. Before, only the project running the install received the restored content. On Windows the repair still replaces the file, so other projects are healed on their next install #3445.A tarball whose integrity pnpm computed during download is now found in the store on the next install. Before, that install downloaded the tarball again once the lockfile recorded the integrity #12562.
pnpm now warns when it cannot hard link packages from an existing store in the pnpm home directory and uses a store on the project's filesystem. This can happen when the project is on another filesystem, such as a bind-mounted workspace in a container. The warning names both stores and suggests setting
storeDir#14505.Resolving and linking dependencies
A peer dependency no longer resolves to two different versions for one package. This happened when the package peer-depends on another package and on one of that package's peers, and it is installed deeper than a direct dependency of the package that provides them #12098.
An optional peer dependency is no longer resolved from another workspace project's package when the project provides one of that package's own peers at a version it rejects. This avoids bogus unmet peer errors #13989.
pnpm no longer reports unmet peer dependency warnings for aliased
npm:peer ranges that a tarball dependency satisfies #11126.pnpm installnow links the executables of auto-installed peer dependencies into the workspace root'snode_modules/.bin, including after a frozen-lockfile reinstall #8511.With
resolutionMode: time-basedandminimumReleaseAgeboth set,pnpm installno longer reports a subdependency as too new when only the time-based cutoff excludes it. Such subdependencies used to fail a strict install withERR_PNPM_NO_MATURE_MATCHING_VERSION, or were added tominimumReleaseAgeExclude#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 thanminimumReleaseAgeonly if no older version matches #16298.With
minimumReleaseAgeset, re-resolving the lockfile no longer rewrites thepeerDependenciesrecorded for a package whose version did not change. This happened when the registry metadata of a package differed from thepackage.jsonin its tarball #13988.pnpm no longer revalidates cached registry metadata when the registry sends
Cache-Control: max-age=0,no-cache, orno-store. It downloads the metadata again, so a version newly published to such a registry is visible on the next install #13487.pnpm's built-in package compatibility database no longer applies to a project's own manifest. A project named like a published package, such as
vue-loader, no longer gains dependencies onpnpm installorpnpm update. User-configuredpackageExtensionsstill apply to project manifests #11700.Relative local tarball paths in
pnpm.overrideswithout an explicitfile:prefix are now rebased correctly for workspace packages #11131.pnpm no longer reports
pnpm-lock.yamlas broken when a project depends on a package namedconstructor. A__proto__key in the lockfile is now kept as a plain entry when pnpm reads or writes the lockfile. It no longer replaces the prototype of the objects pnpm builds from it #11028.With
nodeLinker: pnp, a workspace package can now require another workspace package it depends on #3567. On Windows, workspace dependency paths in the generated.pnp.cjsnow use forward slashes.Performance
pnpm now uses less memory when installing a package whose archive is larger than 64 MiB unpacked, and when installing a runtime from a zip archive, such as Node.js on Windows, Deno, or Bun #14164.
A fresh install reusing a warm global virtual store skips reimporting packages whose target directory is already complete #11112.
A warm
pnpm installreuses 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 updatestill fetches current metadata #13976.Patched dependencies
pnpm installnow repairs apnpm-lock.yamlwhose(patch_hash=<hash>)dependency paths disagree with itspatchedDependenciesmap, including paths that lack the hash their patch calls for. pnpm previously accepted such a lockfile as up to date and kept the old patched files.pnpm install --frozen-lockfilenow fails on such a lockfile withERR_PNPM_INCONSISTENT_PATCH_HASH. It fails withERR_PNPM_UNCHECKABLE_PATCH_HASHwhen 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 installnow fails withERR_PNPM_PATCH_NOT_FOUNDwhen a patch file listed inpatchedDependenciesdoes not exist. It used to fail with a rawENOENTerror and a stack trace #5268.pnpm now fails with
ERR_PNPM_INVALID_PATCHED_DEPENDENCYwhenpatchedDependencieshas an invalid shape or contains a non-string value.engineStrictnow checks the patchedpackage.jsonwhen apatchedDependenciesentry changesengines. A patch that relaxesengines.nodeno longer fails the install against the published range #9603.pnpm patchnow applies the existing patch file to the edit directory of a git-hosted dependency, as it already does for packages from the registry #9699.pnpm patch-commitnow fails with an error whengitcannot be found inPATH. It previously reported that no changes were found #8666.Workspaces and filtering
pnpm installrefreshes injected copies of workspace packages when source projects are rebuilt. Injected copies previously stayed stale untilpnpm install --force#4407.Scripts listed in
syncInjectedDepsAfterScriptsnow update injected dependencies while they run. A watcher on the injected package, such as a dev server, sees each change before the script exits #4410.pnpm installno longer fails for an injected workspace dependency whose package publishes from apublishConfig.directorythat its ownpreparescript builds. The injected copy now picks up that directory oncepreparefinishes building it.pnpm install --frozen-lockfileno longer reports the dependency as outdated while the directory has not been built yet #7811.With
sharedWorkspaceLockfile: false, an injected workspace package that has lifecycle scripts is now hard linked into the projects that depend on it. Before, pnpm left a plain copy, so later edits to the package did not reach those projects #9828.injectWorkspacePackagesnow hard links a workspace dependency declared with a relative path, such asworkspace:../foo, the same way it already does forworkspace:*#10446.pnpm installno longer creates anode_modulessymlink inside thepublishConfig.directoryof a workspace package linked withlinkDirectory. A build tool that cleaned its output directory through that symlink deleted the files of the package's dependencies.pnpm installalso removes a symlink that an earlier install left there #16226.When
verifyDepsBeforeRuntriggers an install before a filteredpnpm runorpnpm 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 importin a workspace now keeps the versions pinned by ayarn.lockinside a workspace project #4385.Installing with
pnprServerset now records the pnpmfile checksum in the lockfile, so a laterpnpm install --frozen-lockfileaccepts that lockfile. A frozen install through the pnpr server now fails if the pnpmfile changed. If the pnpmfile defines areadPackage,afterAllResolvedorpreResolutionhook or custom resolvers, pnpm resolves dependencies locally and prints a warning that the pnpr server was not used #14460.Installing through a pnpr server now links a workspace project at the directory its
publishConfig.directorynames. An install that resolves through a server which does not forward the setting fails withERR_PNPM_PNPR_PUBLISH_DIRECTORY_MISMATCH. The server rejects apublishConfig.directorythat points outside its project.Adding, updating, and removing dependencies
With
autoInstallPeers,pnpm addandpnpm removein a workspace project keep the locked version of a peer dependency the project declares. In a workspace where another project depended on a different version of that package, the peer could switch to that version #11225.Adding a dependency now keeps unrelated transitive dependencies on their locked versions #11456.
pnpm update --recursive <pkg>no longer changes the version of a peer dependency that another workspace project installs automatically. Such a peer could move to a version outside the range the project declares, for example to React 19 in a project that declaresreact: ^18.3.1#14928.pnpm update package@betaon a dependency declared ascatalog:now updates the catalog entry and keepscatalog:inpackage.json. Before, pnpm wrote the resolved specifier topackage.json#13399.pnpm updatenow applies an override that references a catalog with the catalog's new value when the update bumps that catalog entry. Before, the packages the override targets kept the old version in the lockfile #12159.pnpm add <dir>now warns when the added directory declares peer dependencies, aspnpm linkdoes. The directory is saved as alink:dependency, and its peers are not resolved from the project that adds it. Use thefile:protocol to have them resolved #5523.Running scripts and commands
pnpm runandpnpm execno longer install dependencies automatically when the rootpackage.jsonstill keepsoverrides,packageExtensions,patchedDependencies, orignoredOptionalDependenciesin itspnpmfield. 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 topnpm-workspace.yaml#16278.pnpm runno longer reinstalls dependencies when anode_modulesdirectory installed outside CI is used withCI=true, or the other way around #12337.pnpm no longer hangs after a lifecycle script exits while a process it started in the background keeps the script's output open. pnpm stops reading that output one second after the script exits #5730.
A signal sent to pnpm, such as
SIGTERM, now reaches the pnpm that pnpm switches to because ofpackageManagerordevEngines.packageManager, and the one thatpnpm withruns. The signal used to be dropped, so scripts running under that pnpm never got to shut down #9948.pnpm runexits 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 runand lifecycle scripts use the configuredscriptShell, including Git Bash on Windows, whenshellEmulatoris also enabled.shellEmulatorstill runs scripts whenscriptShellis not set. Extra arguments passed topnpm runare quoted for the shell that runs the script, so a Windows path stays intact #14719.pnpm -r run /regexp/now honors thetasksdependsOndeclared for each script the selector matches, like running the script by name does. Matched scripts that depend on each other run in order. Each matched script runs once #15596.Commands run from a POSIX shell through a dependency's own
node_modules/.bin, such asnode_modules/vite/node_modules/.bin/esbuild, no longer fail withMODULE_NOT_FOUND#10189.pnpm rebuildwithnodeLinker: hoistedno longer puts one package's parentnode_modules/.bindirectories on thePATHof the packages it builds after it.Publishing, packing, and deploying
pnpm publishnow 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 deploywith a shared lockfile now copies workspace dependencies into the deploy directory, even whenpackageImportMethodis set tohardlink. Previously, their files were hard-linked to the workspace sources, so editing a source file also changed the deployed copy #12176.pnpm deploy --prodno longer fails withERR_PNPM_OUTDATED_LOCKFILEwhen the deployed project declares adevEngines.runtimewithonFail: download. The runtime stays out of the deployednode_moduleswith the rest of the dev dependencies #15703.pnpm deploy --legacyno longer leaves broken links to nested local dependencies of workspace packages #9575.pnpm deployno longer creates extra directories inside the deploy target and workspace projects when using a relative deploy path #10981.Manifests and configuration files
Settings given on the command line, such as
--registryand--store-dir, now take precedence over the values a pnpmfileupdateConfighook sets #14063.An
updateConfighook that returnsregistriesByScopewithout thedefaultor@jsrentry no longer crashes the install withInvalid URL. A missingdefaultkeeps the configuredregistry, and a missing@jsrfalls back to the built-in JSR registry #15619.The hook's
registryand thedefaultentry of itsregistriesByScopenow set one default registry, which installs,pnpm publish, andpnpm loginall use. If a hook changes both,registrywins. A route that is not a string fails withERR_PNPM_INVALID_UPDATE_CONFIG_RESULT.An
updateConfighook in.pnpmfile.cjshas to useconfig.registriesByScope,config.registriesByPrefix, andconfig.registryOptionsByUrl. These registry lookups were renamed in pnpm 11.23.0 fromconfig.registries,config.namedRegistries, andconfig.registryOptions#15620.An async
updateConfighook that resolves toundefinednow fails withERR_PNPM_CONFIG_IS_UNDEFINED, as a synchronous hook that returnsundefinedalready did.pnpm config set --location=projectandpnpm config delete --location=project, run from a package inside a workspace, now write settings that belong inpnpm-workspace.yamlto the workspace root'spnpm-workspace.yaml. Before, they created a newpnpm-workspace.yamlin the current package, which made that package the workspace root. Settings stored in.npmrcare 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_DIRstill works as a fallback #16275.A
${VAR}placeholder in.npmrcorpnpm-workspace.yamlwhose name matches a built-in object property, such as${toString}, is now treated as an unset variable. It used to be replaced with the source text of a JavaScript function.pnpm now rejects a falsy non-array
packagesfield inpnpm-workspace.yaml, such aspackages: false, with an error. It used to treat the field as omitted.Global packages, pnpm versions, and runtimes
pnpm update --globalnow reinstalls the global packages that pnpm 10 installed into the previous global directory,<global-dir>/5, so their commands are linked into the pnpm homebindirectory again andpnpm list --globallists them. Once every package is migrated, pnpm deletes the previous directory and the commands pnpm 10 linked into the pnpm home #11528.pnpm env remove --globaldeletes Node.js versions that pnpm installed into its own store, including when another tool installed pnpm #8357.pnpm self-updateno longer suggests a downgrade whenminimumReleaseAgeholds back the registry'slatestrelease. It now says that release is still within the cutoff #12006.@pnpm/exeno longer ships a binary for arm64 musl Linux, such as Alpine on ARM. The published binary crashed with a segmentation fault at startup. Installing@pnpm/exeon that platform now fails with an error that suggestsnpm install -g pnpmor pnpm 12 #10443.The macOS and Linux release archives no longer include Windows-only files, such as
node-gyp.cmdand thefastlistexecutables #11352.Windows and WSL
On Windows,
pnpm runnow passes the arguments after the script name to the script as typed. Before,cmdexpanded%VAR%in them and backslashes arrived doubled. Line breaks still arrive as the two characters\n, becausecmdcannot pass them. The command line pnpm prints for the script quotes the arguments the same way on every platform #16257.On Windows, command shims now run tools whose paths contain non-ASCII characters #6999. The PowerShell shims do so in Windows PowerShell 5.1 too #16217.
On Windows, the
.cmdcommand shims innode_modules/.binnow keep a%in the project path. Before, cmd.exe expanded it as a variable reference, so the command received a mangledNODE_PATH#15716.Bin shims in
node_modules/.binrun from Cygwin on Windows again. The shims passed a/cygdrive/c/...path to the Windowsnodefound onPATH, so Node.js failed withCannot find module 'C:\cygdrive\c\...'#12845.pnpm setupno longer writes thepn.ps1,pnpx.ps1, andpnx.ps1PowerShell wrappers. It also removes the ones an earlier setup wrote. PowerShell now runspn,pnpx, andpnxthrough their.cmdwrappers, likepnpmitself. Before, these aliases failed with a "not digitally signed" error wherever the execution policy blocks unsigned scripts #8444.On Windows, globally installed
@pnpm/execommands now run in the invoking PowerShell console and return their exit status #6503.On Windows, installing
@pnpm/exewith npm inside a project now writesnode_modules/.binshims that run the standalone executable #15688.On Windows,
pnpm env use -gandpnpm add -g node@runtime:<version>now replace anode.exein the global bin directory that is a broken symlink. Previously they failed withENOENT#5411.On Windows, pnpm expands nested
%VAR%references inPNPM_HOMEand 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
PATHand aPATHentry still contains an unexpanded variable such as%PNPM_HOME%, the error now names that entry. A variable referenced from the userPathmust be set to a full path and stored as a plain string (REG_SZ) for the entry to expand #5283.On Windows,
pnpm setupno longer garbles non-ASCII characters in existingPathentries #6346.On Windows,
pnpm setuprepairs thePNPM_HOMEregistry type left by older pnpm versions, even when the configured directory has not changed.On Windows, the
ERR_PNPM_BAD_ENV_FOUNDerror ofpnpm setupnow shows the valuePNPM_HOMEis currently set to. It used to show the directory pnpm wanted to set.On Windows,
pnpm installno longer skips a dependency's build script on a later install when the script changes nothing inside the package directory #15667.On Windows, pnpm now retries writing the workspace state file while another process, such as an antivirus scanner, briefly holds it open #14550.
Inspecting dependencies
pnpm licenses listnow reports the actual on-disk package locations when usingnodeLinker: hoistedorshamefully-hoist: true#8589.pnpm licenses list --jsonnow includes every installed copy of a package in itspathsarray, including hoisted copies and isolated installations with different peer dependencies. Its paths also exist on disk when the isolated linker uses a custommodulesDir.pnpm listnow shows the correct path of alink:dependency that points to a directory on another drive on Windows. The path used to be appended to the project directory, such asC:\project\D:\lib, andpnpm list --longcould not show the package's details #10362.pnpm rootnow prints the configuredmodulesDir. It used to printnode_modulesregardless of the setting. A project's ownmodulesDirfrompackageConfigsis printed too #9113.pnpm auditandpnpm audit signaturesnow fail with an error when the lockfile contains unresolvable dependency references #13638.Output and messages
With the default and append-only reporters, installs with
--loglevel warnor--loglevel errornow print the full output of a failed install script. The output of successful scripts, including the root project's own install hooks, stays hidden. With--loglevel warn, pnpm also prints ignored build script warnings.The lockfile verification error now suggests relaxing the policy that flagged an entry only if a fresh resolution still fails and you trust the affected packages. Errors from checks that no policy controls, such as a missing tarball integrity, no longer suggest relaxing a policy #14411.
pnpm run --recursivenow prints GitLab CI collapsible sections that GitLab recognizes. The section markers used to appear as raw text in the job log.Local tarball dependencies using the file protocol are no longer counted as downloaded in the progress banner #1103.
Platinum Sponsors
Gold Sponsors
v11.28.0: pnpm 11.28Compare Source
pnpm 11.28.0 adds the
forceIgnoresPlatformsetting andpnpm update --peer, and fixes many bugs inpnpm deploy,--filter,nodeLinker: hoisted, and custommodulesDirsetups. This release also carries security fixes for shell completion, bin shims on Nix, lifecycle scripts inside a custommodulesDir, anduserAgentplaceholders inpnpm-workspace.yaml.Minor Changes
forceIgnoresPlatformsetting. When it isfalse,pnpm install --forceskips optional dependencies whoseos,cpuorlibcdo not match the host instead of installing all of them. The default staystrue#6133.Patch Changes
Security
pnpm no longer expands environment variables in a
userAgentset in a project'spnpm-workspace.yaml. AuserAgentwith a placeholder in that file is now ignored. Before this fix, pnpm sent the variable's value to the configured registry #15415.pnpm no longer treats packages inside a custom
modulesDiras workspace projects, including one thatpackageConfigssets for a project. Before, with amodulesDirsuch asvendorand apackagespattern such as**, a repeat install ran the lifecycle scripts of dependencies thatallowBuildshad not approved #15412.On Nix, a dependency's bin named like a system utility such as
sedcan no longer redirect a POSIX bin shim or thepnpm,pn,pnpx, andpnxlaunchers. The shims and launchers now ignorenode_modulesand relativePATHentries while they locate their own files. Installing again replaces the shims already innode_modules#14883.Shell completion now omits candidates containing control or invisible formatting characters, and fish completion omits names containing backslashes. Package and script names can no longer inject extra completion records or terminal escape sequences.
Commands that run pnpm again, such as
pnpm runtime setandpnpm env use, no longer re-run a script that only looks like pnpm. A script namedpnpmorpnthat another package installed was run as though it were pnpm.pnpm store prunenow leaves adlxcache root that is a symlink or Windows junction untouched. Cleanup no longer removes directories through that link.Installing packages
pnpm installnow fails at once when a registry or tarball server presents a TLS certificate that fails verification, such as a self-signed or expired one. The error names the certificate problem. Such requests were retried for more than a minute #9134.pnpm installno longer appears to hang when a git dependency is fetched over SSH and ssh asks for a key passphrase or a host key confirmation. pnpm now runs ssh in batch mode, so the install fails right away with the ssh error, and a key that needs a passphrase has to be loaded into an SSH agent first. An ssh command selected throughGIT_SSH_COMMAND,GIT_SSH, or thecore.sshCommandgit setting is kept as is #2227.pnpm no longer crashes on startup when the temporary directory set by
TMPDIR,TEMP, orTMPdoes not exist #4960.pnpm install --silentno longer fails when the install is delegated to pacquet. pnpm also stops passing-s,--logleveland the other reporting flags to pacquet #11936.Fixed
pnpm installfailing withEEXISTwhen a concurrent install cleared the file or directory that was occupying a symlink path. On Windows, a symlink another process is still holding is no longer moved aside and recreated.pnpm installno 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.Installing or adding dependencies no longer fails when a previously installed local tarball file was deleted from disk #8367.
pnpm installnow reads the same local tarball it installs when a dependency's absolutefile:path contains... Such a path could install a different tarball than the one it read, failing withERR_PNPM_TARBALL_INTEGRITY, or fail to resolve at all.pnpm install --frozen-lockfilenow rejects changed local tarballs, even when the previous archive contents are in the store #1889.pnpm addandpnpm installnow support installing bzip2 compressed tarballs #6761.pnpm installnow fetches committed submodules of git dependencies #1470.Interrupting
pnpm installwith Ctrl+C or SIGTERM no longer leaves a temporary lockfile (.pnpm-lock.yaml.*.tmp) behind in the project #1418.pnpm installandpnpm runnow reinstall a single project that was moved or renamed together with itsnode_modules. Before, they reported "Already up to date" while links such as Windows junctions still pointed at the old location #9512.pnpm installnow relinks a direct dependency whose link innode_modulespoints to a missing target. Before, it reported "Already up to date" and left the broken link #9758.pnpm installandpnpm addno longer skip optional dependencies that the Node.js version resolved for adevEngines.runtimerange supports, when the range usesonFail: download. An explicitly setnodeVersionstill takes priority #14628.pnpm installnow uses the running Node.js whendevEngines.runtimedeclares a range withoutonFail: download. Optional dependencies supported by the active Node.js are no longer skipped #15230.pnpm install --engine-strictnow respectsenginesrelaxed byreadPackagehooks in.pnpmfile.cjs#15482.pnpm installnow applies changes to or removal of a globalreadPackagehook when an existing lockfile is present #15136.The project's
.pnpmfile.mjsor.pnpmfile.cjsnow runs after the pnpmfiles of config dependency plugins #9891.A
readPackagehook that sets a dependency range to a value other than a string, such asundefined, now fails the install with an error that names the dependency, the package, and the pnpmfile. Delete the property to remove a dependency #5517.pnpm install --prodand other installs that skipdevDependenciesno longer run thepnpm:devPreinstallscript #7065. They skippreparelifecycle scripts too, as doespnpm installgiven package arguments.pnpm deploydoes not run thepreparescripts of the deployed project #7282.The root project's
preinstallscript now runs before dependencies are resolved and linked. A guard such asnpx only-allow yarncan stop the install before pnpm populatesnode_modules#3760.pnpm install --prod,pnpm fetch --prodandpnpm deploy --prodno 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 sbomandpnpm auditleave it out of--prodresults too. The same applies to--dev. A peer that is not optional is still installed and audited #15344.pnpm prune --prodand production installs now prune excluded development dependencies even when lockfile generation is disabled.pnpm fetchnow also installs the pnpm version thatpnpm-lock.yamlpins, when it differs from the running pnpm. A laterpnpm install --offlinethat switches to the pinned version no longer fails because that version is missing from the store #11808.A dependency that ships a
binding.gypand setsgypfile: falseno longer gets thenode-gyp rebuildinstall script pnpm synthesizes for it. Such a dependency needs noallowBuildsentry and is no longer listed under "Ignored build scripts".pnpm installno longer addsallowBuildsplaceholder entries topnpm-workspace.yamlwhen it runs in CI or without a terminal. Interactive installs still add them #11574.Installing through a pnpr server now installs a project's peer dependencies when
autoInstallPeersis enabled. A project that declared only peer dependencies failed withERR_PNPM_OUTDATED_LOCKFILEor skipped its peers #14833.Resolving and linking dependencies
pnpm now installs a dependency that a package also declares as an optional peer dependency, for example
lightningcssin some vite builds. The dependency was missing fromnode_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
overridesnow 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.packageExtensionsandoverridesentries with a ranged selector (such as@<Xor@*) no longer match a dependency that has nopackage.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 withERR_PNPM_TRUST_DOWNGRADEeven when an older version satisfied its range.pnpm self-updatepicks its target version the same way. A request for an exact version still fails #14176.pnpm installandpnpm updatenow resolve a dependency range to the newest matching version that is not deprecated. A version already recorded in the lockfile is still used #15128.pnpm addandpnpm removeno longer move unrelated transitive dependencies to other versions. Adding a package and then removing it now leavespnpm-lock.yamlunchanged. Before, the dependencies of auto-installed peers andnpm:aliased subdependencies could move to a newer version that was already in the lockfile #11859.pnpm dedupenow moves transitive dependencies to the version acatalog:dependency pins, as it already did for versions written directly inpackage.json.pnpm install --ignore-pnpmfileno longer removespnpmfileChecksumfrom an up-to-datepnpm-lock.yaml.pnpm install --frozen-lockfile --ignore-pnpmfileno longer fails withERR_PNPM_LOCKFILE_CONFIG_MISMATCHwhen the lockfile records apnpmfileChecksum. A command that resolves dependencies with the pnpmfile ignored still writes the lockfile without it #10944.pnpm install --frozen-lockfilenow succeeds when an optional dependency was unresolvable and skipped by the install that wrote the lockfile. Previously, frozen installs failed withERR_PNPM_OUTDATED_LOCKFILE. The notice states that the dependency could not be resolved and names the requested range #3960.pnpm no longer rewrites
packageManagerDependenciesinpnpm-lock.yamlwhen that block pins@pnpm/exebesidepnpm. The rewrite ran on every command, sopnpm listleft a clean working tree dirty, andpnpm versionthen refused to run #14926.pnpm importand fresh resolutions now recordintegrityfor git-hosted tarballs, such ascodeload.github.comURLs, even when the tarball is already in the store #13338.Merging lockfiles now preserves recorded configuration fields such as
overrides,neverBuiltDependencies,patchedDependencies,packageExtensionsChecksum,settings, andcatalogs#8366.A lockfile entry whose resolution is unchanged now keeps its recorded
deprecatedmessage #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.With
nodeLinker: hoisted,hoistWorkspacePackagesnow links each workspace project thathoistPatternorpublicHoistPatternselects into the rootnode_modules, unless a hoisted package or a root dependency already uses its name. The project's bins are linked into the rootnode_modules/.bin#7553.Workspace projects that
hoistPatternorpublicHoistPatternselects are now hoisted on every install. A project added to the workspace was not hoisted untilnode_moduleswas deleted and reinstalled. A workspace that installs nothing from a registry hoisted none of its projects at all #3642.With
nodeLinker: hoisted,pnpm installnow removes the commands of the packages it removes fromnode_modules/.bin, such as a nested copy deduped into the rootnode_modules#7568.pnpm installnow links a dependency's bin even when the bin's file does not exist yet, such as a workspace package's bin that a build script creates after install. Previously pnpm printed aFailed to create binwarning. The command then stayed missing untilnode_moduleswas removed #10007, #10216.Executable linking now makes a bin executable for every user. A target with only some execute permission bits set was left non-executable for other users #3699. Bin linking leaves workspace and linked dependency files outside
node_modulesunchanged.Dependencies and executable binaries are now correctly linked and accessible for workspace packages using
publishConfig.directoryandpublishConfig.linkDirectory#8338.Workspaces and filtering
Fixed
pnpm installfor workspace projects reached through a symlink, such as apackagesdirectory that links to a folder outside the workspace. pnpm now installs their dependencies, and the links in theirnode_modulesresolve #1044.A dependency declared with
catalog:now counts as a workspace dependency when its catalog entry points at a workspace project, for exampleworkspace:*#15587. WithlinkWorkspacePackagesenabled, so does annpm:alias of a workspace project, such as"math-alias": "npm:math@^1.0.0".pnpm -r runruns that project first.--filter <pkg>...selects it.A
workspace:dependency now resolves to a workspace project whose version is not valid semver, such as1or1.0.workspace:*,workspace:^, andworkspace:~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.3matches a project at0.5.6-next.3+f60facc#6483. A workspace project with build metadata is no longer skipped when it matches the requested range and has the same version precedence as the registry package #2812.Secondary dependencies now prefer the version resolved by the local project's direct dependencies over versions from sibling workspace projects #7191.
pnpm installnow 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-lockfilenow fails withERR_PNPM_OUTDATED_LOCKFILEwhenpnpm-lock.yamllists 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 --frozen-lockfilenow fails when a workspace package's version no longer satisfies the range that a dependent workspace project declares for it. This includes injected workspace dependencies #7823.pnpm install -rnow installs every workspace project whenrecursiveInstallis set tofalseinpnpm-workspace.yaml#7504.pnpm installwith--filternow installs only the dependencies of the selected projects when usingnodeLinker: hoisted#8882.pnpm installnow updates an injected workspace dependency after that package's own dependencies change, whenshared-workspace-lockfileisfalse#7209.syncInjectedDepsAfterScriptsnow copies files into injected dependencies whennode_modulesis 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.Fixed injected workspace dependency synchronization failing with
EPERMon Windows when removing nested directories.Workspace discovery now returns one project per directory when multiple manifest formats are present. It selects
package.json, thenpackage.json5, thenpackage.yaml#3027.Wildcards in negated
packagespatterns ofpnpm-workspace.yamlnow match directories whose names start with a dot. For example,!packages/**now also excludespackages/.dev/toolwhen another pattern includes.devexplicitly.pnpm now warns when a workspace install covers a project that has its own
pnpm-workspace.yaml. The nested file's settings, such aspatchedDependencies, do not apply when the outer workspace installs that project. pnpm reads settings only from thepnpm-workspace.yamlat the workspace root #11724.pnpm now warns when
shared-workspace-lockfileis passed on the command line outside a workspace #1617.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 inpnpm-workspace.yaml#8718. It also selects projects that files were moved out of when git detects the move as a rename #15481.--filternow evaluates selectors in order, so later inclusion filters can re-include packages that an earlier exclusion filter excluded #9354.Directory filters such as
--filter=./packages/*now select projects when the current directory was entered with a lowercase drive letter on Windows, likec:\repo#5500.Custom modulesDir
pnpm run,pnpm exec,pnpm versionhooks, and the lifecycle scripts a project runs during install now find commands in a custommodulesDir, and bin shims resolve dependency paths there #3604. Project.hooksscripts are read from the configured modules directory. Installs resolved by pnpr now preserve configured modules and executable directories.pnpm binnow reports the configured executable directory. In a workspace whose projects keep their own lockfiles, apackageConfigsentry that gives one project its ownmodulesDiris followed too.Tools installed in a custom
modulesDircan load CommonJS plugins installed there, the same way they would fromnode_modules. When executables are symlinks, as withpreferSymlinkedExecutablesor the hoisted linker, this works forpnpm run,pnpm exec,pnpm versionhooks, and the lifecycle scripts a project runs during install. A symlinked tool started directly from a shell does not get it. Paths containing the platform path-list separator do not receive this fallback.extendNodePath: falsedisables this fallback #3604.A repeat
pnpm installin a workspace with a custommodulesDirnow takes the up-to-date fast path. Before, pnpm looked for each workspace project's dependencies innode_modulesand ran a full install every time.Adding, updating, and removing dependencies
pnpm addnow keeps the specifier areadPackagehook provides when the hook rewrites the requested one. When the hook removes the dependency instead, the add skips it and reports why. Previously the add wrote the request either way, and the hook undid it on the next read, sopnpm install --frozen-lockfilefailed #15156.pnpm addnow saves changes topackage.jsonbefore running lifecycle scripts, so a postinstall script failure leaves the added dependency inpackage.json#8627.pnpm addnow saves the requested exact version when adding a dependency, even when the manifest already contains a version range #6040.pnpm addandpnpm installkeep an emptypeerDependencies,dependencies,devDependencies, oroptionalDependenciesfield that was already inpackage.json. pnpm still drops such a field when it removes the last entry itself, aspnpm removedoes #5096.Added a
--peerflag topnpm updateto update ranges inpeerDependencies#8081.pnpm updatenow keeps a version range whose shape has no save prefix, such as<= 3.0.0or>=1.0.0 <2.0.0, when the updated version still satisfies it. Before,<= 3.0.0became^3.0.0#6714.pnpm update <name>now updates a peer dependency that pnpm installed automatically #10486.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 asminimumReleaseAgeused to reject that version before the update could replace it #9953.pnpm update --prodno longer installs devDependencies when run in a project installed with--prod#8038.pnpm update --interactive --workspacenow allows external dependencies to be updated.pnpm outdatedandpnpm updatenow applyminimumReleaseAgeto GitHub Actions.minimumReleaseAgeExcludeentries match action names such asactions/checkout#13923.pnpm removenow runs the project's ownpreuninstall,uninstall, andpostuninstallscripts.preuninstallanduninstallrun before dependencies are unlinked. A failure in either stage aborts the removal.postuninstallruns after unlinking completes. TheignoreScriptssetting and--lockfile-onlyskip all three stages #3276.pnpm remove -rnow fails if any requested dependency is absent from all selected workspace projects. Validation respects--save-prod,--save-dev, and--save-optionaland completes before modifying project manifests #2319.pnpm removenow accepts--trust-lockfileand--no-trust-lockfileto control supply-chain policy checks while removing a package #14406.pnpm unlinknow removes thelink:dependency thatpnpm link <dir>added topackage.json. The linked package is removed fromnode_modulesand the lockfile. Alink:dependency to another directory is kept #4219.minimumReleaseAgeExcludePruneandtrustPolicyExcludePrunenow work in workspaces withshared-workspace-lockfile=false. Once every project has been installed, pnpm drops an entry only if no project lockfile records it. UndecidedallowBuildsentries are pruned the same way #14612.pnpm importnow converts dependencies that use Yarn'spatch:protocol. The dependency keeps the version it patches, and the patch file is added topatchedDependenciesinpnpm-workspace.yaml. If the patch file is missing, pnpm prints a warning and imports the dependency without the patch #10278.pnpm importin a workspace now keeps the versions pinned by the rootyarn.lock,package-lock.json, ornpm-shrinkwrap.jsonwhen another workspace project's range allows a newer version. Before, the root project got the newest version in its range #4385.pnpm patch-commitnow resolves default patch directory locations when passed a package name or package specifier (such aspnpm patch-commit <pkg>orpnpm patch-commit <pkg>@<version>).pnpm patch-commitnow updates the lockfile snapshot and prunes removed dependencies when the patch modifiespackage.json#6866.pnpm patch-commitnow falls back to copying package files when hard linking fails.make-dedicated-lockfileno longer removes fields such asmainandtypesfrom thepublishConfigof the project'spackage.json. It now restorespackage.jsonwhen it cannot move the originalnode_modulesback to its place. The error then names.tmp_node_modules, where the originalnode_moduleswas left. The command refuses to run while that directory exists, so a retry cannot overwrite it.Running scripts and commands
A script that pnpm runs without a terminal now ends when pnpm itself is killed. Killing pnpm's process group, as Playwright's
webServerdoes 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>andpnpm -r <command>now run a command installed in the selected projects' dependencies when none of them has a script by that name. This matchespnpm <command>in a single project.pnpm runwith--filteror-rstill reports the missing script #10151.pnpm execandpnpm dlxnow setnpm_execpath,INIT_CWD,npm_node_execpath, andNODEfor child processes #7037. Scripts thatpnpxandpnxrun now get pnpm itself asnpm_execpath. A script that ran$npm_execpath installthere ranpnpm dlx install.pnpm execnow sets thePWDenvironment variable to the directory the command runs in. Shells and tools that readPWDnow report the logical path of a workspace package reached through a symlink #1550.A script that runs
pnpm runno longer adds duplicatenode_modules/.binandnode-gyp-binentries toPATH#5352.Concurrent
pnpm runandpnpm execcommands now serialize their dependency installs #14551.pnpm run --recursiveno longer reports interrupted scripts as lifecycle failures afterCtrl+C.pnpm restartnow 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 install-testnow accepts--no-bailwhen executed directly and in recursive runs #3777.pnpm dlxnow 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.A
runtime:version range that contains||or a space, such as adevEngines.runtimeversion of^22.18.0 || ^24.0.0, now installs the requested runtime. pnpm used to install the npm package with the same name, such asnode#14817.When the configured
scriptShelldoes 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.If a script is killed by a signal that pnpm survives, such as SIGPIPE, the error now names the signal:
Command failed with signal SIGPIPE.#9821.Publishing, packing, and deploying
pnpm publishnow resolvesworkspace:dependencies from workspace manifests whennode_modulesis not installed. Previously, publishing withoutnode_modulesfailed withERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL#6567.pnpm publishnow honorspublishConfig["@scope:registry"]for a package in that scope. It takes precedence over the registry set for the same scope in.npmrcand overpublishConfig.registry#12071.pnpm packandpnpm publishnow include bundled dependencies when using the isolated linker. This covers workspace packages and the dependencies of each bundled package. Bundled dependencies are also included whenpublishConfig.directoryselects 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 packleaves out symlinks that point outside the package #8208.pnpm packnow honors thefilesfield ofpackage.yamlandpackage.json5manifests. Git-hosted and injected local dependencies that use these manifests now honor it too #7906. The archive includes exactly onepackage.jsonwhen the project uses an alternative manifest format, even when.npmignoreorfilesexcludes the source file.pnpm packnow preserves file executable permissions in the packed tarball when source files are executable on disk.pnpm publishandpnpm packnow report a missingversionornamefield on a workspace dependency. Previously, pnpm reported that the dependency was not installed #4164.pnpm publishandpnpm packnow report an error when a bin script has a shebang line ending with CRLF #7311.pnpm deploynow copies thepackageManageranddevEngines.packageManagerfields of the workspace rootpackage.jsoninto the deployedpackage.json, unless the deployed project pins a package manager itself #9079.pnpm deploynow puts the virtual store atvirtualStoreDir, resolved against the deploy directory. A shared-lockfile deploy recordsvirtualStoreDirin the deployedpnpm-workspace.yaml. With the global virtual store enabled or an absolutevirtualStoreDir, the deploy still usesnode_modules/.pnpm#8787.pnpm deploynow respects--package-import-methodpassed on the command line and reports the package import method correctly #7593.pnpm deployno longer triggers an install when running scripts in a read-only deployed filesystem #11617.A legacy
pnpm deploywithnode-linker=hoistednow puts the deployed project's direct dependencies at the top of the deployednode_modules#9671.pnpm deploy --legacyno longer rewritesnode_modules/.pnpm-workspace-state-v1.jsonin the source workspace. The nextverifyDepsBeforeRuncheck there reported the workspace as out of date #15352.Manifests and configuration files
Fixed
pnpm versionfailing on projects using apackage.yamlmanifest.Fixed
pnpm initcreating an extrapackage.jsonwhenpackage.yamlis already present.pnpm now preserves CRLF line endings when it modifies project manifests.
pnpm versionnow applies pending bumps to private workspace packages. A private package's changelog is written to its committedCHANGELOG.md, also whenversioning.changelog.storageisregistry#13736, #13519.pnpm change checknow validates the pending change intents in.changeset/. It fails when an intent names a package that is not in the workspace or cannot be released..npmrcfiles now support npm's${VAR?}placeholder. It expands to the value ofVAR, or to an empty string without a warning whenVARis unset #14404.pnpm now expands environment variables in
_auth.authTokenvalues loaded from globalconfig.yamlandpnpm_config__auth#12828.pnpm now warns when an empty environment variable removes an
.npmrcauthentication token. Authentication environment warnings now name the affected key #4806.pnpm now keeps the configured default registry when
_authholds 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 treats a missing global
config.yaml,auth.ini, or other optional config file as absent in Node.js-compatible runtimes such as StackBlitz WebContainers. Commands such aspnpm --versionfailed there withENOENT#14030.pnpm loginnow 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, asnpm logindoes #12055.pnpm doctornow checks the configured default registry and sends its credentials. It used to always pinghttps://registry.npmjs.org/#15618.Global packages, pnpm versions, and runtimes
Global commands such as
pnpm add --global,pnpm list --global, andpnpm bin --globalnow run with the pnpm you invoked, even in a project that pins another pnpm version. Previously, a pin withonFail: "download"switched them to the pinned pnpm, and a pinned pnpm 10 or older failed because its global bin directory was not inPATH#14531.pnpm add -g,pnpm update -g, andpnpm remove -gnow recover a global package group whose entirenode_modulesdirectory was deleted.pnpm remove -gleaves such a group's command shims in the global bin directory #15093. These commands no longer fail withERR_PNPM_PACKAGE_MANIFEST_IO_ERRORwhen 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 --globalnow skips a global package installed from afile:path that no longer exists, prints a warning, and updates the remaining global packages. Previously the whole update failed withERR_PNPM_LINKED_PKG_DIR_NOT_FOUND#12533.pnpm update -gno longer asks more than once for approval of the same immaturename@versionwhenminimumReleaseAgeStrictis enabled #15091.pnpm self-updaterun in a project that pins pnpm throughpackageManagerordevEngines.packageManagernow also updates the global pnpm, as it does outside a project #14747.pnpm self-updateno longer leaves the previous pnpm in the global packages when it was installed as@pnpm/exe.pnpm ls -gnow lists a single pnpm #14709.pnpm setupno longer deletes aliases and other lines that sit between a# pnpmcomment and the pnpm block in a shell startup file #7067.pnpm env removenow cleans up dangling Node.js executables and symlinks. Surviving global commands remain intact.Node.js runtime resolution now supports Windows ARM64. Node.js 20 and newer resolve native
win-arm64builds, and older versions fall back towin-x64under emulation #7123.Windows and WSL
pn,pnpx,pnx, andpnpmnow run when Git Bash, MSYS2, or Cygwin launches them through a Windows path such asC:\Users\me\node_modules\pnpm\pn. The aliases used to fail to find the pnpm installed beside them, or hand the call to an unrelated one #14884.Fallback
.cmdand.ps1Windows wrappers in@pnpm/exenow propagate the exit status of the invokedpnpmcommand #14826.On Windows, bin shims run from Git Bash, MSYS2, or Cygwin now pass
NODE_PATHto Node.js as Windows paths. A project installed from cmd or PowerShell gave its bins aNODE_PATHunder the Git install directory when they ran from Git Bash. Installing again replaces the shims already innode_modules#3360.pnpm installin 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 withEACCES#6155.On Windows, pnpm now retries saving
pnpm-lock.yamlfor up to a minute while another process holds the file open. The save used to fail at once withEPERM,EBUSY, or "Access is denied" #9461.On Windows, pnpm now fails within about a second when it cannot move a
node_modulesdirectory installed by another package manager because a file in it is in use. The error names the directory and suggests stopping the process that uses it. pnpm used to retry for a minute and then print a rawEPERMstack trace #7505.pnpm now escapes trailing dots and spaces in
node_modules/.pnpmdirectory 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.pnpm installnow resolves local tarballs specified with bare UNC paths on Windows #1669.pnpm now recognizes local paths with forward slashes on Windows.
On Windows,
pnpm addandpnpm updatenow write relativefile:andlink:specifiers with forward slashes topackage.jsonand the lockfile. They used to write backslashes, so the same project produced different files on Windows and on other systems #7497, #9687.Fixed scripts failing with errors such as
'an-compile' is not recognizedwhenscriptShellis set tocmd.exeon Windows #7181.Inspecting dependencies
pnpm auditandpnpm audit signaturesnow 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 auditnow 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=updatenow fixes vulnerabilities in dependencies declared through an npm alias. A specifier such as"foo": "npm:vulnerable-pkg@1.0.0"moves to the patched version and keeps the alias. Versions pinned with a leading=are fixed as well #15155.pnpm audit --fixnow prunes redundant overrides when one vulnerable range is a subset of another for the same package #8577.Running
pnpm listinside a workspace package without--recursiveor a filter now lists only the current package #14494.pnpm licenses listdoes the same. Use--recursiveor--filterto list the licenses of other workspace projects #5689.pnpm list --only-projectsnow prints every project selected with--filteror--recursive, including a project that has no workspace dependencies #9770. It also lists the workspace projects whensharedWorkspaceLockfileisfalse#7151, and a project that setspublishConfig.directory#10635. It no longer reports packages innode_modulesthat are missing from the lockfile #9528.pnpm licenses listfailed or reported nothing in a workspace withsharedWorkspaceLockfile: false. It now reads the lockfile of each selected project #10140.With
nodeLinker: hoisted,pnpm licenses listreported paths undernode_modules/.pnpmthat do not exist. It now reports the directory where the hoisted linker placed each package #8589.pnpm outdatedandpnpm -r outdatednow fail withERR_PNPM_NO_PACKAGE_IN_DEPENDENCIESwhen a requested package selector does not match any dependency in the inspected projects #2319.pnpm -r outdated --jsonnow 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 examplevue@2.7.14 (dev)#7693.pnpm sbomfiltered to a single workspace project no longer replaces the project's ownlicenseorbugsfield with the workspace root's value when the project's value is blank. The same applies to anauthor,description,license,repository, orbugsfield set tonull#14882.pnpm store statusno longer reports packages with build or postinstall scripts as modified in the store. When packages were mutated, it now lists only those packages and no longer suggests runningpnpm install --force#919.pnpm peers checkand theERR_PNPM_PEER_DEP_ISSUESerror now group peer dependency issues under the workspace project they were found in #15351.The
pnpm:peer-dependency-issueslog event, which--reporter ndjsonprints, no longer lists peers silenced bypeerDependencyRules.ignoreMissingunderconflictsorintersections#8295.Output and messages
The error for an incompatible pnpm-lock.yaml now reports the lockfileVersion the file was generated with and the lockfileVersion the current pnpm supports. The error also warns that recreating the lockfile with
--forcemay break the application and suggests installing the pnpm version that generated the lockfile #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.Resolution no longer logs an error when a package metadata request fails and resolution succeeds via cached metadata #2522.
The ignored build scripts warning and the update notice are printed as plain lines when output is not a terminal, in CI, or with
--reporter append-only. They were drawn inside a box that broke apart in CI logs #9421.pnpm runwith--loglevelset towarn,error, orsilent(or the sameloglevelsetting) no longer prints the$ <command>line before a script, nor the summary of the install thatverifyDepsBeforeRunruns first. Both are info-level output #8944.pnpm addnow warns when replacing an existing dependency with a specifier pointing to a different source #14869.pnpm updateno longer warns "Skip adding ... to the default catalog" for a dependency that already usescatalog:#13715.pnpm remove --helpno longer shows a[@<version>]suffix in its usage line. The command accepts package names only #7751.Bash completion now completes script names that contain a colon, such as
pnpm run test:utopnpm run test:unit#5482.Platinum Sponsors
Gold Sponsors
v11.27.1: pnpm 11.27.1Compare Source
Patch Changes
pn,pnpx, andpnxnow run the pnpm installed alongside them. They used to look pnpm up onPATH. That failed when the directory holding them was not onPATH, and it silently handed the call to an unrelated pnpm when one came first there #14803.The
@zkochan/cmd-shimpackage is now available as@pnpm/bins.cmd-shim.pnpm cache list-registriesnow prints the registry URL, matchingpnpm cache view. It printedhttps%3A+registry.npmjs.orgbefore and printshttps://registry.npmjs.org/now #15046.pnpm deployno longer installs the dependencies of the workspace root project into the deploy directory #6437.Install warnings no longer carry the text of a package's deprecation notice. The warning names the deprecated package and version, and the
pnpm:deprecationevent no longer carries the notice either.pnpm viewstill shows it on request.A deprecation warning now names the newest version of the package that is not deprecated, and says when reaching it means widening the range you declared:
pnpm works this out from the metadata it already fetched, so it costs no extra request. An install that reuses the lockfile without fetching metadata names no version.
pnpm strips control characters from the package name and version in a deprecation warning, and from the notice
pnpm outdated --longprints.The text sanitizer now also strips the Unicode line and paragraph separators U+2028 and U+2029.
pnpm exec <command>andpnpm <command>run from a subdirectory of a project now find the executables installed in the project'snode_modules/.bin. The command still runs in the subdirectory.PNPM_PACKAGE_NAMEnames the project #5068.pnpm execandpnpm dlxnow wait for the command to finish shutting down afterCtrl+C. A signal sent to pnpm alone now reaches the command, the way it does withpnpm run. pnpm used to exit on the interrupt and terminate the command while it was still shutting down #7374.Warnings about ignored environment variables in project
.npmrccredentials now link to the npmrc documentation pnpm/pnpm#15051.pnpm audit --interactive --fix=updateno longer opens a second prompt for selecting dependencies to update #14927.Fixed
pnpm deduperequiring a second pass after bumping a direct dependency inpackage.jsonpnpm/pnpm#14987.pnpm deploynow writes plain versions for registry dependencies with peer dependencies in the deployedpackage.json. The deployed lockfile retains the resolved peer bindings. npm aliases keep their target package names #14873.pnpm publishnow allows a detached Git HEAD in CI, including checkouts of release tags. The working tree must still be clean. Branch and remote-history checks still apply when HEAD is attached pnpm/pnpm#5894.pnpm dlxandpnxnow prompt to approve dependency build scripts in interactive terminals. Cached packages with pending builds also prompt for approval. Without an interactive terminal, use--allow-buildto allow the required builds. Fixes pnpm/pnpm#14943.pnpm install --forcenow removes obsolete dependency links inside virtual-store packages when their dependencies change. Invalid dependency names are ignored during obsolete-link cleanup #15039.pnpm add -gandpnpm update -gnow ignore incomplete unrelated global package groups when every command from the replaced group is retained. Operations that could remove a global command still require complete ownership information.Resolving a Node.js runtime now fails when unofficial-builds.nodejs.org cannot be reached. pnpm used to ignore that failure and leave the musl builds out of
pnpm-lock.yaml.pnpm updatethen wrote a different lockfile on a machine whose network blocks the mirror pnpm/pnpm#14813.pnpm now deduplicates a package whose child dependency resolved an optional peer in one workspace project but not in another. Two copies of
nextcould appear when only some projects could reachstyled-jsx's optionalbabel-plugin-macrospeer #14800.Fixed shell completion of package scripts for
pnpm runandpnpm run-scriptpnpm/pnpm#15034.Bash completion now preserves literal script names containing glob characters and shell punctuation in pnpm v11 and v12.
pnpm sbomnow publishes a valid URL in the CycloneDXexternalReferences[].urland the SPDXhomepage. An npm shorthand such asvercel/msorgitlab:group/subgroup/projectis expanded to thegit+httpsURL npm derives for it. An scp-style remote such asgit@github.com:vercel/ms.gitis expanded the same way. Any other URL is published in its normalized form, without embedded credentials. A value that names no repository, an email address for example, is left out. pnpm used to publish the raw value, so a shorthand produced a URL that consumers such as Dependency-Track reject pnpm/pnpm#14773.pnpm setupnow describes the displayed configuration changes as "the following configuration changes."pnpm --versionnow reports why the pnpm version a project pins cannot be installed or recorded, then prints the version of the running CLI. It used to fail, which made the command unusable where the filesystem is read-only.pnpm --versionalso honors--store-dirand its--storealias now #14831.pnpm install --forcenow reinstalls dependencies when the manifest and lockfile are unchanged. It previously reported "Already up to date" without reinstalling. Files changed innode_modulesare restored when the store content is intact. Combining--forcewith--frozen-storenow reports a configuration conflict on repeat installs #919.pnpm installnow installs git-hosted dependencies without preparing them when their builds are explicitly denied byallowBuilds. Dependencies that require preparation still need an explicit allow or deny decision pnpm/pnpm#10522.pnpm runtime setandpnpm env usenow use the pnpm version that started the command. They could run a different installed pnpm when the command was started through Corepack or another wrapper.The install summary now names the version each dependency resolved to when
node-linkerishoisted. It also lists what an install restores afternode_modulesis deleted, and both sides of a version change. The summary showed the range recorded inpackage.json, or nothing at all #15161.The
@pnpm/npm-lifecyclepackage is now available as@pnpm/exec.npm-lifecycle.Fixed
minimumReleaseAgemaking pnpm download a package's full metadata again on every install. The cached copy carried a validator the registry could not match, so pnpm could never revalidate it pnpm/pnpm#15103.pnpm now measures a
pnpm.overridesentry written as a bare path, such as./local-dep, from the directory holdingpnpm-workspace.yaml. It used to be measured from each package the override rewrote, so the dependency linked to a directory that does not exist #11131.pnpm now preserves scalar YAML anchors and aliases when editing
pnpm-workspace.yaml. Removing the entry that defines an anchor keeps surviving aliases valid. Entries updated to different values are written separately #8245.pnpm now preserves comments and existing key order when updating
package.yaml. New keys are appended to their mapping pnpm/pnpm#2008.pnpm install --prodno longer downloads the registry packages that only a devDependency reaches #881.pnpm update --globalno longer reinstalls a global package when its dependency graph resolves to what is already installed. It reportsAlready up to datepnpm/pnpm#12002.The
minimumReleaseAgeapproval prompt now counts and displays each package version once pnpm/pnpm#15083.pnpm runno longer sends a script a secondSIGINTwhenCtrl+Cis pressed in a terminal. A script that shuts down on the firstSIGINTand exits at once on a second used to die before its shutdown finished #7374.pnpm now reads a
pnpm-workspace.yamlwhosetaskssection uses a setting only pnpm 12 acts on, such asconcurrencyGroup. A task's unrecognized fields are ignored, unless the field only differs in case fromconcurrencyordependsOn, which pnpm reports as a typo.The warning about unrecognized top-level settings now names
cargo,concurrencyGroups, andpipelinesas pnpm 12 settings.pnpm -r list --jsonnow prints one JSON array. It printed a separate array for each project whensharedWorkspaceLockfilewasfalse, so the output could not be parsed.pnpm -r listnow reads each project's own modules directory when the projects keep their own lockfiles, so--longand--parseablereport the packages that project installed #15011.A signal sent to pnpm while it runs without a terminal, as a container runtime or a service manager does, now reaches the script even when the shell running it stays the script's parent. pnpm then waits for the script to finish shutting down. Such a signal used to end the shell at once or stay with it, and the script was never told to stop #7374.
Fixed
minimumReleaseAgebeing skipped for packages served by a registry that returns the same ETag for abbreviated and full package metadata pnpm/pnpm#14925.pnpm installnow returns "Already up to date" in a workspace wherededupeDirectDepsleft a project without anode_modulesdirectory of its own. Such a project forced a full install on every run.Installs in different projects that share a global virtual store no longer fail on Windows with
Access is deniedwhile repairing the same slot #15114.pnpm sbomnow emits a license value as a CycloneDX expression only when it is a valid SPDX license expression. Anything else is emitted as a CycloneDX license name pnpm/pnpm#14786.A dependency's own bins can no longer take over another package's bin shim. The POSIX shims pnpm generates used to look up their shell helpers on
PATH, where a dependency's bins come first #14837. Reinstalling replaces the shims already in yournode_modules. On Cygwin, MSYS2, and WSL the shims still take their Windows path conversion fromPATH, so a dependency can still redirect them there.POSIX bin shims now convert a Windows-form path such as
C:\node_modules\.bin\tsccorrectly. The shim mangled the backslashes in such a path and could not reach the package it runs. Installing again replaces the shims already innode_modules#14867.pnpm packnow writes tarball entries grouped by file extension and file name, the order npm uses. Packages that ship many same-named files, such as template collections, pack much smaller #14766.A command run in a project that the workspace does not include now acts on that project alone. A project is outside the workspace when it has a manifest of its own and no pattern in the
packagessetting selects it, or when a!pattern excludes it. A directory with no manifest of its own, such as a package's source directory, still belongs to the workspace.pnpm installin an excluded project used to install every project in the workspace #3561.POSIX bin shims now take
cygpathandwslpathfrom the system default path on Cygwin, MSYS2, and WSL2. The shims looked both helpers up onPATH, where a dependency's own bins come first, so a dependency could redirect another package's shim. Installing again replaces the shims already innode_modules#14866.pnpm updateandpnpm audit --fix=updateno longer copy dependencies added bypackageExtensions, areadPackagehook, or an override intopackage.json. Those dependencies keep the specifier the hook or override gives them.pnpm update --latestno longer resolves past that specifier.pnpm audit --fix=updatenow warns when one of them pins a vulnerable version. The warning points atpnpm audit --fix#14928.Platinum Sponsors
Gold Sponsors
v11.27.0: pnpm 11.27Compare Source
Minor Changes
nodeDownloadMirrorscan now be set in the global config file (config.yaml) and through thePNPM_CONFIG_NODE_DOWNLOAD_MIRRORSenvironment variable, so a Node.js download mirror can be configured once for a machine instead of in every workspace #12124, #13611.Added a new setting
trustPolicyExcludePrune(default:false). When enabled,pnpm add,pnpm update, andpnpm removeprune the entries oftrustPolicyExcludeinpnpm-workspace.yamlthat 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.Patch Changes
pnpm now reads the
packageManager,devEngines.packageManagerand runtime pins from the workspace root'spackage.jsonwhenlockfileDiris set. A project that moved its lockfile lost the pins it declared there #14633.Fixed
pnpm add -g,pnpm update -g, andpnpm remove -gmutating global bins or install directories after only partially reading an installed package group. If any declared package manifest is missing, malformed, or unreadable, pnpm now fails before activation or removal and leaves the existing global installation intact pnpm/pnpm#13796.fetch-timeoutnow limits how long a request may make no progress. The timer restarts on every chunk that arrives. A large download over a slow connection is no longer aborted while data is still coming in. A connection that stops delivering data still fails afterfetch-timeout#14604.pnpm peers checkno longer reports a peer dependency declared asworkspace:^,workspace:~, or a bareworkspace:as unmet. pnpm reported these as unmet whatever version the linked workspace project supplied #14770.A
readPackagehook that edits its argument in place no longer changes what a later install in the same command resolves. Adeprecatednotice read from the lockfile no longer carries over to another install either #13988.pnpm installnow auto-installs missing transitive peers when workspace projects share a dependency at different depths. This also removes incomplete duplicate peer contexts from the lockfile. Fixes pnpm/pnpm#14840.GitHub Actions updates now stop if an action reference changes while its versions are being resolved. Unrelated workflow edits are preserved.
GitHub Actions homepage links no longer expose server credentials. GitHub server URLs now require HTTPS, with HTTP allowed only for loopback hosts.
pnpm licenses listnow reports the runtime downloaded throughdevEngines.runtimewithonFail: "download". The command previously failed withERR_PNPM_UNSUPPORTED_PACKAGE_TYPE#14172.pnpm no longer creates a project
pnpm-lock.yamlwhendevEngines.packageManager.onFailisdownloadand lockfile writing is turned off withlockfile: falseor--no-lockfile. pnpm still switches to the pinned version #14728.A
registryor@scope:registryset in an.npmrcnow wins over the registry apnpm logincredential stored in the globalconfig.yamlpoints at. Previously, after logging in to one registry, installs in a project whose.npmrcnamed a private registry went to the logged-in registry instead. They now go to the registry the.npmrcnames #14614.A patch that gives a dependency a
preinstall,install, orpostinstallscript, or abinding.gyp, now runs that build. pnpm asks for build approval first, so the package is listed under "Ignored build scripts" until it is allowed to build. pnpm 12 ran nothing, and pnpm 11 ran it without asking #14648.Registries that share a host but differ by URL path — one JFrog Artifactory, Nexus, AWS CodeArtifact or GitLab Packages instance serving several repositories — now get a metadata cache directory each. Previously they shared one, so resolving a package from one of them could answer with another's versions, integrity hashes and tarball URLs and fail with
ERR_PNPM_TARBALL_URL_MISMATCH#13558.The URL scheme is part of the cache directory name too, so an
httpregistry can no longer hand its metadata — which can be rewritten in transit — to a resolution configured forhttpsat the same host.The first install after upgrading refetches registry metadata once. The package store is untouched.
pnpm cache viewnow labels each entry with the full registry URL. It printedregistry.npmjs.orgbefore and printshttps://registry.npmjs.org/now.pnpm cache list-registriesandpnpm cache listprint the new directory names. Scripts that parse either command need updating.Updated the embedded Node.js release keys to the current canonical
nodejs/release-keyslist.pnpm sbomnow omits package author fields when the manifest author name is empty or contains only whitespace pnpm/pnpm#14685. In a filtered or split workspace run, only a project with noauthorfield inherits the workspace root's author.pnpm sbom --sbom-format spdxnow writescreationInfo.createdwith whole seconds, such as2026-09-08T10:38:21Z. The timestamp carried fractional seconds, which strict SPDX consumers rejected #14684.Windows filesystem operations now retry permission errors for up to one second. Permanent permission errors previously delayed failure by a minute. Sharing and lock violations retain their one-minute retry budget pnpm/pnpm#14682.
pnpm now writes
node_modules/.package-map.jsononly whennodeExperimentalPackageMapis enabled. Nothing reads the file without that setting. An install that stops writing the map removes the one a previous install left.pnpm now unpacks a downloaded runtime archive into a randomly named directory inside the store. It previously used a predictable path, where another user of a shared store could plant a symlink and redirect the write outside the store (GHSA-vwc7-r8mq-g2x9).
Platinum Sponsors
Gold Sponsors
v11.26.0: pnpm 11.26Compare Source
Minor Changes
Catalogs can now resolve workspace dependencies through the
workspace:protocol.pnpm removeandpnpm updatenow accept--trust-lockfile,--no-trust-lockfile,--trust-policy,--trust-policy-exclude, and--trust-policy-ignore-after.pnpm removechecks the whole lockfile against the active policies unless--trust-lockfileis set.Added
pnpm change checkfor CI validation of package versions against theversioning.epicsbands andversioning.fixedgroups inpnpm-workspace.yaml.Patch Changes
Fetch and tarball errors and retry logs now hide URL credentials, query strings, and fragments that could expose secrets.
Fixed a race during config dependency updates that could redirect a lockfile write through a symlink #14322.
pnpm add --allow-build=!<pkg>now correctly denies builds, including in global installs.pnpm approve-builds <pkg>andpnpm approve-builds !<pkg>now save decisions even when the package is not awaiting approval, with a warning #14067.Fixed
pnpm audit --fixfailing without a value or when followed by another flag.pnpm audit --fix=overridenow respectssaveExactandsavePrefixwhen writing overrides #13261, #11523.pnpm auditnow excludes ignored advisories from vulnerability totals and severity counts, and reports them separately #14535.pnpm deployno longer requiresinjectWorkspacePackages. If a workspace dependency's peer has multiple possible versions, deployment reportsERR_PNPM_DEPLOY_AMBIGUOUS_PEERwith the conflicting versions. Pin the peer withoverridesto deploy without injection #9386.Fixed concurrent installs sharing a store occasionally failing with an
ENOENTerror while importing a package file #14353.Fixed installation failures when a linked local dependency provides a peer dependency also provided by an ancestor, including with
pnpm deploy --legacy.pnpm install --node-linker=hoistedno longer downloads skipped optional dependencies whennode_modulesalready exists #14139.Fixed
pnpm installrejecting a symlinked lockfile when config dependencies are unchanged. Updates to config dependencies also preserve lockfiles with a byte order mark. Writes through symlinked lockfiles remain blocked #14372.pnpm installnow relinks workspace packages whenpublishConfig.linkDirectorychanges. Frozen installs require the lockfile to be regenerated #14488.Auto-installed optional peers now satisfy their declared range even when the workspace root uses a version outside that range #13867.
Fixed global virtual store paths for dependency cycles to consistently account for the runtime engine when dependencies have allowed builds #14341.
Standalone installations now preserve the bundled
node-gypfiles needed to build native dependencies.Downloaded runtimes are now available to dependency lifecycle scripts during installation.
Node.js downloads from
nodeDownloadMirrorsnow use URL-scoped npm credentials, including bearer tokens, basic auth, andtokenHelper#14334.Fixed
globalDirandglobalBinDirhandling in global configuration and environment variables, including~/expansion. This fixespnpm add -gfailing afterpnpm config set -g global-bin-dir#14336.The JavaScript pnpm can again switch to the project's pinned pnpm version on hosts without a matching native binary. If the requested version requires an unavailable native binary, the error now identifies the unsupported host #13622.
Global
pnpm configcommands now skip project package manager version switching, allowing authentication to be configured before downloading the pinned version #14463.pnpm self-update,pnpm with, and automatic version switching no longer wait through registry retries when a configured registry has no signatures andregistry.npmjs.orgis unavailable #14483.Fixed argument forwarding on Windows with
shellEmulatorenabled. Trailing backslashes, line breaks, and literal shell expressions are preserved #14548.Relative
scriptShellpaths now resolve from the workspace root. Bare command names such asbashstill usePATH#14422.pnpm importnow preserves the project-local lockfile whenlockfileDirpoints elsewhere and restores the destination lockfile on failure. Branch lockfile imports leave the shared lockfile unchanged #14563.catalogModeand--save-catalogno longer move local paths, tarballs, orworkspace:<path>specifiers into catalogs #14437.--side-effects-cache,--no-side-effects-cache, andPNPM_CONFIG_SIDE_EFFECTS_CACHEnow toggle only the local cache, preserving any remote cache configured insideEffectsCache.pnpm unpublishnow handles registry two-factor authentication challenges through web authentication or a one-time password prompt #14464.pnpm outdatedandpnpm updatenow follow GitHub Actions references using self-repository syntax, such asuses: $/.github/actions/setup.pnpm removenow accepts--unsafe-perm.Platinum Sponsors
Gold Sponsors
v11.25.0: pnpm 11.25Compare Source
Minor Changes
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
remoteSideEffectsCachesetting. A workspace names the eligibleorganizationandpackages; everything describing the act of signing —publish,keyId,builderId,trustedKeys,privateKeyand the provenance fields — is refused inpnpm-workspace.yamland read from the global config file or the environment instead.Added macOS and Windows x64 and arm64 support to remote shared build artifacts pnpm/pnpm#13771.
Added the
audit.ignorePrunesetting. When set totrue,pnpm audit --fixremoves ignored GHSA entries that no longer appear in the audit report.Generalized the experimental shared-artifact protocol so candidates and signed payloads identify a discriminated subject. Dependency side effects use package and source-integrity subjects, while workspace tasks use project and task subjects.
This changes shared-artifact request bodies and signed payloads. A pnpr server and its clients have to be on matching versions.
pnpm initnow 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 owndevEngines.packageManager/packageManagerpin #7490.The version is read from the
latesttag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry,offline, or alatestthat theminimumReleaseAge/trustPolicysettings reject —pnpm initpins the running version as before, and never fails or hangs on the lookup. Alatestthat is older than the running pnpm is never pinned either.A
scopeset in a project'spnpm-workspace.yamlis now ignored, with a warning naming where to set it instead.pnpm loginrecords the scope as a@scope:registryroute in the machine-globalauth.ini, which outranks~/.npmrcin every project — so a repository-committed file could redirect a scope such as@acmefor all of a user's other projects after one routine login. Use--scope, thePNPM_CONFIG_SCOPEenvironment 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-fromskips exactly the work that passed during a matching interrupted or failedpnpm -r run/pnpm -r execinvocation. When no compatible state exists, pnpm retains its graph-based resume behavior.Allowed
pnpm update --patchesto refresh registry revisions through a configured pnpr server while retaining locked package versions.Added explicit registry revision selection with
<version>+rNandpnpm update --patchesfor refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries.Workspace install, rebuild, pack, publish, stage, and lifecycle work now starts as soon as its dependencies finish instead of waiting for an unrelated topological group.
pnpm stage approvenow approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. The whole batch is approved with a single one-time password, and pnpm asks for a new one only once the registry stops accepting it. Inside a workspace, the selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry.Added per-task concurrency limits to workspace task orchestration. Set
tasks.<name>.concurrencyinpnpm-workspace.yamlto limit how many instances of that task may run across workspace projects at once:Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the
registriessetting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts.sideEffectsCachenow 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: true,sideEffectsCacheReadonly,remoteSideEffectsCache, and itsorganizationfield 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: truenow blocks writing to the cache, and setting it alongsidesideEffectsCache: falsegives 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 runandpnpm -r execnow 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
taskssection inpnpm-workspace.yamldeclares what a task depends on, using the^convention:^namemeans the named task in each of the project's workspace dependencies; a barenamemeans the task in the same project; an entry with nodependsOndeclares an empty dependency list. A task with no entry behaves asdependsOn: ['^<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. SettingignoreWorkspaceCycles: truedowngrades the error to a warning: the cycle's tasks run in an arbitrary order relative to each other.--resume-fromnow 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 theverifyDepsBeforeRuncheck);--jsonemits the tasks and their resolved dependency edges.--workspace-concurrency=1, or the graph forces the scripts to run one after another).Patch Changes
An
_authentry in the global config file no longer decides which registry packages come from when something else says. Aregistryorregistriesdeclared inpnpm-workspace.yamlor the global config now wins over the route inferred from a stored credential, which still applies where nothing else declares one. Thepnpm_config__authenvironment variable is unchanged: it stays the way to point a CI runner at a mandated proxy, and still overrides what a repository declares.Prevent installs through a symlinked
node_modulesdirectory from rewriting the target checkout pnpm/pnpm#14286.Treat empty scripts selected by a regular expression as missing before running dependent tasks.
The options type of the
fetchcommand now declaresallowBuilds, a setting its handler already forwarded to the installer. Type-level only — whatpnpm fetchdoes is unchanged.Filter hidden scripts matched by a regular expression during recursive runs when a visible script also matches.
Fixed automatically switched pnpm versions forcing all descendant pnpm processes to use the same version pnpm/pnpm#14309.
Fixed
ERR_PNPM_UNUSED_PATCHvalidation during incremental installs pnpm/pnpm#13692.Fixed
pnpm deploy --prodfailing when an excluded dev dependency was also declared as an optional peer dependency pnpm/pnpm#14302.pnpm update -gno longer downgrades a global package.--latestresolves thelatestdist-tag, which can point at an older release than the one installed — afterpnpm add -g <pkg>@next, for instance #14270.pnpm update -galso no longer changes the pnpm version. pnpm's own global install belongs topnpm self-update#14270.Copying a built package to its other hoisted locations no longer replaces the destination directory. With
nodeLinker: hoisted, that replacement deleted the dependencies nested inside the destination'snode_modules, and made concurrent copies of the same build chunk fail withERR_PNPM_ENOENT: no such file or directory, rename '.../node_modules/_tmp_...'#12880.pnpm updateno longer replaces the specifier a project declares for a dependency that is also listed inoverrides. Acatalog:reference stays acatalog:reference, and a declared range stays as written, instead of being rewritten to the version the override resolved to #12115.pnpm updateno longer moves the range a project declares for a dependency thatoverridesalso lists, even when the override repeats that range verbatim. Previously the updatedpackage.jsondisagreed with the lockfile, so the nextpnpm install --frozen-lockfilefailed with a specifier mismatch #14224.Make
pnpm add --lockfile-onlyskip dependency linking pnpm/pnpm#14286.--productionis accepted again as an alias of--prodoninstall,fetch,prune,update,list,why, andsbom, and the install thatverifyDepsBeforeRunreproduces is now spelled with--prod.pnpm runno longer aborts with "unexpected argument '--production' found" after a production-only install #14147.The progress output no longer overwrites the lines above it once it grows taller than the terminal window #14270.
Restoring a dependency's build from the remote side-effects cache no longer downloads files the store already holds.
Forward
patchedDependencieshashes andpackageExtensionsto pnpr so server-side resolution preserves patches and package extensions in the lockfile and installed packages.Published the workspace task graph and scheduler as
@pnpm/workspace.task-schedulerso other workspace commands can use the same dependency-aware scheduling as recursive run and exec.The environment variables for the remote side-effects cache are named for the setting they configure:
PNPM_SIDE_EFFECTS_CACHE_REMOTE_KEY_ID,..._BUILDER_ID,..._IMAGE_DIGEST,..._ARCHITECTURE_BASELINE,..._PRIVATE_KEY,..._BUILD_ENV,..._TRUSTED_KEYSand..._PUBLISH. ThePNPM_REMOTE_SIDE_EFFECTS_CACHE_*names keep working, and the new one wins when both are set.A
devEngines.packageManagerrange pin on pnpm is now recorded inpnpm-lock.yaml'spackageManagerDependencieswhen 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 thanpnpm add/pnpm self-update) left the project without the shared resolution the pin exists to provide.Fixed recursive
runcleanup on Windows when a lifecycle script fails while another script's process tree is still running.The update notification now suggests
pnpm self-updatewhenPNPM_HOMEmanages the pnpm in use, and the standalone install script otherwise — under Corepack, or when another package manager installed pnpm.pnpm self-updateunder Corepack names the standalone install script too.Enforce
allowBuildswhen a prepared git dependency is reused from the shared store, and use the lockfile's canonical git resolution ID in approval suggestions.Topologically sorting workspace projects now runs in linear time, fixing installs and lockfile updates that stalled for seconds on workspaces with thousands of projects forming deep dependency chains #14149, #14151.
Platinum Sponsors
Gold Sponsors
v11.24.0: pnpm 11.24Compare Source
Minor Changes
Patch Changes
Fixed pnpm v11 incorrectly reporting
confirmModulesPurgeas unrecognized when set inpnpm-workspace.yaml. The Rust CLI now identifies the unsupported option as a pnpm v11 setting instead of suggesting an unrelated setting.pnpm install --frozen-lockfileno longer fails withERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILEwhen the pinned pnpm version recorded inpnpm-lock.yamlhas 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 (afile: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-lockfilefailing withERR_PNPM_OUTDATED_LOCKFILEwhen a branch lockfile predates the removal of a dependency, or its move to another dependency group #13966. A dependency that no project declares anymore is no longer reinstated by the merge, and the packages it was the only path to are dropped with it.Batch workspace publishing accepts a shared scope-specific credential, rejects mismatched credentials for a registry before publishing, and runs the
publishandpostpublishscripts after each completed registry group pnpm/pnpm#14101.The Rust CLI now honors five settings it recognized but ignored:
updateNotifier,legacyDirFiltering,initAuthorName/initAuthorEmail/initAuthorUrl,initLicense, andinitVersion.pnpm installandpnpm addcheck once a day for a newer pnpm and print how to get it (turn it off withupdateNotifier: false); a{<dir>}filter selector can go back to matching the subtree below the directory withlegacyDirFiltering: true; andpnpm initwrites the configured author, license, and version into thepackage.jsonit scaffolds.PNPM_CONFIG_INIT_VERSIONis now read as well.maxsockets, npm's spelling ofmaxSockets, is no longer ignored: both spellings are read frompnpm-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
lastUpdateChecktimestamp 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.legacyDirFilteringno longer reaches the workspace-root selectors pnpm generates for itself: the!{<workspace-root>}exclusion a recursiverun/exec/add/testappends, and the{<workspace-root>}inclusion--workspace-rootappends. Read as subtree matches they named every project below the root, so a recursive command under the setting selected nothing at all, and--workspace-rootpulled in every project below the root instead of the root alone #14101.pnpm install --frozen-lockfileno longer fails whenpnpm-lock.yamlrecords 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.v11.23.0: pnpm 11.23Compare Source
Minor Changes
pnpm config getandpnpm config listnow show the settings pnpm acts on under their documented names:registriesshows 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@jsrscope and thenpmjsandghprefixes — unless pointed elsewhere. Previouslypnpm config get registriesprintedundefined.updateandauditshow the effective sections, whichever spelling set them. The deprecated internal spellings (updateConfig,auditConfig,auditLevel) are no longer listed.catalogsshows the complete resolved catalog set — the singularcatalogblock is itsdefaultentry — whichever spelling declared it.registryand@scope:registryentries show the merged routes rather than raw.npmrcvalues, so they always agree with theregistriesview.Settings that no supported pnpm version recognizes get their own warning. A key in the global config file that this version of pnpm does not read is no longer reported with advice to move it to a project-level
pnpm-workspace.yaml(where it would be ignored too); the warning now says the setting is not recognized by this version of pnpm, names the pnpm version that does read it when there is one (for example,globalShimsis a pnpm v12 setting), and suggests the closest real setting name when the key looks like a typo. Unrecognized and non-camelCase keys in a project'spnpm-workspace.yaml, previously ignored silently, are now reported the same way.pnpm config get <key>andpnpm get <key>no longer print config-load warnings, so a script capturing the value gets the value alone.The
importPackagepnpmfile hook is deprecated. pnpm now prints a warning when a pnpmfile defines it, and the hook will be removed in the next major version. It also opts the installation out of the parallel package importer, making installation slower. If you rely on this hook, comment on #14101.node_modules/.modules.yamlno 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
enableGlobalVirtualStoreis on, every process pnpm spawns for the project (pnpm run,pnpm exec, lifecycle scripts) now receives aNODE_PATHpointing at the project's hoistednode_modules, plus aNODE_OPTIONS--importflag that registers a resolve hook restoringNODE_PATHlookups 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-pathconfig dependency pnpm/pnpm#9618. Tools run bypnpm dlxresolve 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
timefield, soresolutionMode: time-basedreads the full metadata document only from the registries that need it:registry.npmjs.orgomitstimefrom 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:registrySupportsTimeFieldanswered 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 atimefield npmjs does not serve. The answer is now per registry, andregistrySupportsTimeFieldremains 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
registriessetting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one'sserverType— 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
serverTypereaches 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.ioon 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:registryin a developer's~/.npmrcno longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message.This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned.
The
registriessetting 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.serverTypetells pnpm how the registry lays out its tarball URLs, which decides whether a URL can be omitted frompnpm-lock.yaml:npm— the registry behaves likeregistry.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 frompnpm-lock.yamlinstead of being written out for every scoped package pnpm/get-npm-tarball-url#16.scopeslists the@-prefixed scopes that resolve from this registry. A bare'@'is the scope-less default registry, the one theregistrysetting names.prefixis 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.orgcontinues to behave asnpmwithout being declared. Because the lockfile depends onserverType, it is read frompnpm-workspace.yamlonly — aserverTypein the globalconfig.yamlis 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.Migrating
The older
registriesshape, a map of<scope>: <url>strings, still works and needs no change:namedRegistriesis deprecated in favor of theprefixfield, and is still read for prefixesregistriesdoes not declare.toLockfileResolutionandisCanonicalRegistryTarballUrlnow take their registry and layout as an options object rather than positional arguments, so@pnpm/lockfile.utilsand@pnpm/resolving.tarball-urlget a major bump.An install that had to re-hash store files to verify them now reports it. If that cost more than a second, it says how long —
The integrity of N files was checked in 2.5s.— and if it was quick but covered more than a thousand files, it names the cause instead: their timestamps changed since the store recorded them, which a backup tool, an antivirus scan or a copied store can do.Added
virtualStoreType, which names where the virtual store lives — one store per machine, or one per project:It is the canonical spelling of
enableGlobalVirtualStore, which keeps working. When a project sets both,virtualStoreTypewins. It can also be set throughPNPM_CONFIG_VIRTUAL_STORE_TYPEand read back withpnpm config get virtualStoreType. The default is unchanged —project, so the shared store stays opt-in.The setting is independent of
nodeLinker.isolatedandpnpboth work with either store type, andhoistedwrites no virtual store at all, so it is unaffected.Patch Changes
pnpm add --allow-buildnow adds to theallowBuildsentries already inpnpm-workspace.yamlinstead of replacing them #13872.Kept pending build approvals available after removing an unrelated dependency.
pnpm approve-buildsnow removesonlyBuiltDependencies,onlyBuiltDependenciesFile,neverBuiltDependencies, andignoredBuiltDependenciesfrompnpm-workspace.yamlwhen it writesallowBuilds. Those settings were replaced byallowBuildsin pnpm 11 and silently ignored since, so a workspace migrated from pnpm 10 kept them around looking active.pnpm auditno longer reports a patched version that was never published or is deprecated. The inferred patched range (e.g.>=4.17.24from<=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.1when4.17.24does not exist and4.18.0is deprecated). When no published version satisfies the range, the report showsPatched versions: None. This also preventspnpm audit --fixfrom adding overrides orminimumReleaseAgeExcludeentries for patches that do not exist #13824.pnpm audit --fixandpnpm audit --fix updateno longer add aminimumReleaseAgeExcludeentry 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 theminimumReleaseAgegate #11563.The
--jsonoutput ofpnpm auditnow returnspatched_versions: nullfor advisories whose inferred patch is not available (never published, skipped, yanked, or deprecated), making it easier for tooling to distinguish "no fix available" from "fix available at version X".Fixed
pnpm patch-commitin project and edit paths containing non-ASCII characters.The package and bump pickers of
pnpm changenow size their page from the terminal height instead of always showing 7 rows. They fall back to 7 rows when the terminal height is unknownpnpm/pnpm#13815.Canceling a
pnpm changeprompt with Ctrl-c no longer prints a stack trace. It reportsChange canceledand exits with a success status, like the other interactive commands #13814.Re-fetch full registry metadata when
minimumReleaseAgeis enabled and an abbreviated packument'stimemap 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 atarball) now takes its tarball URL from the registry's packument instead of deriving it from the registry URL, so migrating one costs an extra metadata request. On a registry that serves tarballs from a path pnpm cannot derive, GitLab's group endpoint for one, installing such a config dependency failed with a 404 while the same package installed fine as a regular dependency #13765.Fixed
PNPM_CONFIG_NODE_VERSIONbeing ignored when setting the Node.js version used for compatibility checks.A custom fetcher can no longer replace the archive integrity that
pnpm-lock.yamlpins: the locked value is restored after acanFetchorfetchhook 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
pnpmfilesetting (a single path or an ordered list), and hands custom fetchers nativelocalTarballandremoteTarballcallbacks — including on a fresh install that has to compute a missing tarball integrity, which is then reused by later offline installs. File maps a fetcher returns are accepted only when they match what those native callbacks extracted.Fixed an issue where running
pnpm dedupe --checkin projects withnodeLinker: hoistedwould cause dependencies to be moved out ofnode_modulesintonode_modules/.ignored.pnpm deploy --prodandpnpm deploy --no-optionalno longer list the excluded dependency groups in the deployedpackage.jsonandpnpm-lock.yaml. The deployed lockfile referenced packages that the deploy left out of its graph, so installing in the deploy directory afterwards created dangling symlinks #13623.Don't treat files like
license16.jsonas a package license when deciding if the workspace LICENSE file should be included in the packed package.pnpm exec --recursive --no-reporter-hide-prefixno longer prints a blank prefixed line after each chunk of a command's output, and no longer splits a line in two when it straddles a chunk boundary.Fixed
404errors when installing from a registry that serves scoped packages only from a percent-encoded path, such as GitHub Enterprise Server. Outsideregistry.npmjs.org, a tarball URL that encodes the scope separator as%2for%2Fis no longer mistaken for one that pnpm can rebuild from the package name, version, and registry, so it is kept inpnpm-lock.yamland requested verbatim on the next install #13534.Fixed
trustPolicyExcludeandminimumReleaseAgeExcludebeing ignored when set to a single string instead of a list. The value was read one character at a time, so the exclusion never matched the package it named — and a*anywhere in it matched every package, silently switching the policy off.pnpm initnow pins the exact pnpm version instead of a^range, and records it in thepackageManagerfield alongsidedevEngines.packageManager. Corepack reads onlypackageManagerand accepts nothing but an exact version, so it rejected the generatedpackage.jsonwith "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-managerstill scaffolds a manifest without any pin. In pnpm 12,pnpm initalso honorsinitTypeand its--init-typeflag, so the manifest it writes is the same one pnpm 11 writes.Fixed an issue where package overrides were written into the metadata cache, causing removed overrides to keep applying on subsequent installs pnpm/pnpm#13918.
On Windows, upgrading pnpm no longer leaves a stale
pnpm.ps1behind. PowerShell resolvespnpm.ps1ahead ofpnpm.cmd, so a shim written by an older installation kept running the previous version. Linking the pnpm CLI's bins now deletes it #13919.Fixed an inconsistency where
minimumReleaseAgeExclude(andtrustPolicyExclude) wildcard/bare-name rules behaved differently in the evaluator and normalizer. A bare rule now consistently evaluates as matching every version, preventing unexpected behavior and silent widening of version policy exemptions when pnpm rewrites the workspace manifest pnpm/pnpm#13725.A frozen install no longer rewrites the
packageManagerDependenciesblock ofpnpm-lock.yaml. When the pnpm version pinned bydevEngines.packageManager(or bypackageManager) is missing from the lockfile or no longer matches it,--frozen-lockfilenow fails withERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILEinstead of resolving the version and saving it, so a manifest whose pin was bumped without regenerating the lockfile can no longer pass CI #14009.A git dependency installed over HTTPS from a hosted repository now keeps its branch, tag, or version range in the specifier recorded in
package.json. It was written back without one, so the nextpnpm updatemoved the dependency to the repository's default branch #13999.Fixed
pnpm update --global --latestfailing 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, annpm: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 #12854.Fix recursive
pnpm update <name>@<version>so an exact pinned update stays scoped to the requested version line: copies of the same package on another major line — or, for a0.xrequest, 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.jsonand the depending package keeps it in itsnode_modules/.bin.Fixed
pnpm install --merge-git-branch-lockfilesdeleting the per-branch lockfiles when thelockfilesetting isfalse. Such an install never reads them, so it has nothing to merge them into and now leaves them alone.Fixed
pnpm installsometimes not exiting after printingDone in Xs#12297.Fixed pnpm failing to read
.modules.yamlfiles containing long dependency paths #13875. The manifest is now parsed as JSON (the format pnpm writes it in), falling back to the YAML parser only for manifests written by old pnpm versions.With
preferSymlinkedExecutables,NODE_PATHagain points at the virtual store of the workspace root when pnpm is run from inside a workspace package, so scripts can resolve dependencies that live only in the hoisted store #13912.Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win pnpm/pnpm#13976.
pnpm pkg getandpnpm pkg setnow accept hyphens inside a dot-notation property path, sopnpm pkg get dependencies.some-package-namereads the key instead of failing withERR_PNPM_UNEXPECTED_TOKEN_IN_PROPERTY_PATH. The bracketed and quoted forms already worked and are unchanged.A resolve request now carries the client's
resolutionMode, so an install delegated to a pnpr server picks versions the way the client would.time-basedandlowest-directreached the server as nothing at all, leaving it on itshighestdefault: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored.This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving
highest; the protocol is still experimental and unversioned.Fixed
pnpminstalls using pnpr to honor the client'sautoInstallPeers,dedupePeers, andexcludeLinksFromLockfilesettings pnpm/pnpm#13389.pnpm removenow prunes undecided entries ("set this to true or false") fromallowBuildsinpnpm-workspace.yamlwhensharedWorkspaceLockfile: trueand the corresponding packages are removed pnpm/pnpm#13892.Fixed workspace discovery for
pnpm-workspace.yamlfiles without apackagesfield so commands only consider the workspace root instead of recursively scanning nested projects #14047.A runtime installed through
devEngines.runtimenow matches the host whensupportedArchitectureslists several platforms. Listingos: [darwin, linux]andcpu: [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 sbomnow fails withERR_PNPM_SBOM_MISSING_IMPORTERSwhenpnpm-lock.yamlhas no entry for a selected project, instead of writing an SBOM that under-reports that project's dependencies. Previously this crashed withCannot read properties of undefined (reading 'devDependencies').pnpm self-updatenow rewrites a simpledevEngines.packageManager.versionrange (^/~) to the newly installed version, keeping the operator — matching howpnpm updateandpnpm runtime setrewrite ranges. Complex ranges such as>=8.0.0that the new version satisfies are still left unchanged #13935.pnpm self-update <tag>no longer downgrades when the dist-tag points at the pnpm version already running and that version is younger thanminimumReleaseAge. The maturity cutoff moved the tag back to the previous mature release, sopnpm self-update next-12on v12.0.0-rc.4 switched to v12.0.0-rc.3.pnpm set-scriptnow updatespackage.jsoninstead of failing withERR_PNPM_NOT_IMPLEMENTEDpnpm/pnpm#13956.pnpm updatenow preserves the existing range operator when updating a prerelease dependency. See #7002.Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges (
"*","^1.2.3") rather than theworkspace:protocol. WithpreferWorkspacePackagesenabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install.Added
fetchWarnTimeoutMsandfetchMinSpeedKiBpsto the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters pnpm/pnpm#12042.An override change is now absorbed by the fast lockfile update even when another, unchanged override uses the
catalog:protocol. Previously anycatalog:-valued override forced a full re-resolution whenever the override list changed, which could move unrelated packages in the lockfile (for example afterpnpm audit --fixadded an override).Packed workspace package manifests now preserve dependency order, making repeated
pnpm packoutput deterministic #10167.pnpm update <name>@<version>now fails withERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEPwhen 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 theoverridesentry 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-downgradeno longer aborts the install withERR_PNPM_MISSING_TIMEon registries that serve no per-versiontimefield whenminimumReleaseAgeIgnoreMissingTimeis set. The trust check reads the same publish dates theminimumReleaseAgecheck does, so it now honors the same opt-in and skips the affected package with a warning #12446.minimumReleaseAgeIgnoreMissingTimeno longer lets a lockfile entry the registry does not list pass theminimumReleaseAgecheck 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-
timewarning now names the check it is reporting on, so a package whoseminimumReleaseAgeandtrustPolicychecks are both skipped warns about both instead of only the first.pnpm update <pkg>@<version>now updates only the selected packages and leaves unrelated dependencies unchanged. A selector that renames the package it installs —pnpm update <alias>@npm:<pkg>@<version>or thejsr:equivalent — now targets the package the alias installs rather than the alias.Fixed
verifyDepsBeforeRunbeing ignored when set toinstall,warn,error, orpromptthrough thePNPM_CONFIG_VERIFY_DEPS_BEFORE_RUNenvironment variable or the--config.verify-deps-before-runflag #13816. Only the boolean values were accepted before, so a string value was silently dropped.pnpm version <bump>with--dry-runno longer editspackage.jsonfiles. It now only reports the bumps it would make, and skips the working tree check, the version lifecycle scripts, the commit, and the tagpnpm/pnpm#13953.Platinum Sponsors
Gold Sponsors
v11.22.0: pnpm 11.22Compare Source
Minor Changes
Added
pnpm cache path, which prints the directory pnpm uses for its metadata cache. CI setups can use it to cache that directory — including the lockfile verification log, which lets a job skip re-checking an unchanged lockfile against the configured supply-chain policies.--config.config-dirno longer reaches the config through a project'spnpm-workspace.yaml, and neither do the--config.spellings of the other settings a project manifest may no longer contribute (--config.pnpm-home-dir,--config.workspace-dir,--config.global-pkg-dir,--config.root-project-manifest-dir). None of them was ever a supported way to set those directories: pnpm resolves them from the environment, and these flags took effect only because the project-manifest merge re-applied the command line afterwards. The dedicated flags, such as--dirand--global-dir, are unaffected #13629.pnpm config setrefuses to write a setting to a project'spnpm-workspace.yamlthat pnpm does not read from there, rather than leaving a key in the file that does nothing. Those settings areconfigDir,pnpmHomeDir,stateDirand the others that name machine-level state. The command fails withERR_PNPM_CONFIG_SET_NOT_A_PROJECT_SETTING, naming where the setting does belong when it belongs somewhere.pnpm config deletestill clears one that a file already carries, in whichever spelling it uses #13629.Added a new setting
minimumReleaseAgeExcludePrune. When enabled,pnpm add,pnpm update, andpnpm removeprune the entries ofminimumReleaseAgeExcludeinpnpm-workspace.yamlthat 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
cleanupUnusedCatalogstocatalogPrune, so that catalog pruning and release-age exclude pruning use one vocabulary.cleanupUnusedCatalogscontinues to work; when both are set,catalogPrunewins.A project's
pnpm-workspace.yamlcan no longer choose where pnpm keeps its credentials, its own installation, or the registry it downloads its next version from. One of those settings isconfigDir, which decided wherepnpm loginwrites the granted token.bin,dir,globalBinDir,globalDir,npmrcAuthFile,pnpmHomeDir,stateDir,userconfigandworkspaceDirare ignored there now too, and pnpm warns about the ones it finds.cacheDirandstoreDirare unaffected #13629.Resolving a Node.js runtime version (
devEngines.runtime/runtime:specifiers) is now much faster: the per-version release metadata is cached in the pnpm cache directory after its signature is verified, and an exact stable version such asruntime:22.23.2no 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 firstnodeinvocation in a project pinning an already-downloaded runtime #13899.Patch Changes
Fixed intermittent
ERR_PNPM_ENOENTandERR_PNPM_ENOTEMPTYerrors while renaming_tmp_*directories during installation withnodeLinker: hoisted, in workspaces that also usepatchedDependencies.pnpm addno longer re-resolves the dependency graph whenpnpm-lock.yamlalready 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 inpackage.jsonand 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, aworkspace:/catalog:/git/tarball specifier,--save-peer, an overridden package, acatalogModeother thanmanual, and — underresolutionMode: time-basedorlowest-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 -gandpnpm update -gactivate a new version by moving that one link instead of rewriting every shim. A command can no longer be missing fromPATHwhile an install is in progress, and a failed install leaves the previous version in place.pnpm audit --fixandpnpm audit --fix updateno longer addminimumReleaseAgeExcludeentries for patched versions that were published before theminimumReleaseAgecutoff. The publish time of each minimum patched version is now checked against the registry metadata, and only versions young enough to be blocked by the age gate get an exclusion entry #11563.pnpm add <pkg>@<version>andpnpm update <pkg>@<version>under a non-manualcatalogModenow move the catalog entry's resolution to the requested version. Previously, when the catalog entry was a range that covered the requested version but resolved to a different one, the request was dropped silently: nothing was installed, nothing was written, and no error was raised.A project that wasn't part of an install that moved a catalog entry now follows the entry the next time it is installed. It used to keep the version the entry resolved to before — a version the entry no longer allowed — and no later install corrected it, so one catalog entry ended up resolved to two versions.
pnpm add <pkg>@<version>andpnpm update <pkg>@<version>undercatalogMode: strictno longer fail withERR_PNPM_CATALOG_VERSION_MISMATCHwhen the catalog entry is a range that the wanted version satisfies. The dependency keeps using the catalog; only a version that really falls outside the catalog's range is rejected #13715.A changed
catalogsorpnpm.overridesblock no longer has to be the only change forpnpm installto 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 installnow updates the lockfile in place even when several kinds of changes happened since the last install — for example a removed dependency together with a widenedignoredOptionalDependencieslist, or a dependency edit alongside a patch or settings change. Previously any combination of changes forced a full re-resolution #13763.pnpm deployinjects workspace dependencies again, so the deploy directory is self-contained instead of symlinking back into the source workspace #13754. EnablinginjectWorkspacePackageswithdedupeInjectedDepsdisabled now also rewrites already-linked workspace dependencies to injected copies.pnpm deploy --no-optionalno longer writes a lockfile whose snapshots reference optional dependencies that the deploy excluded.Removing the last dependency that references a catalog entry via the fast lockfile update no longer leaves the stale catalog entry in
pnpm-lock.yaml.A git dependency whose clone (or shallow fetch) fails now reports which package it belongs to, under the
ERR_PNPM_GIT_FETCH_FAILEDcode, 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 withpnpm update <package>#13743.An
integrityrecorded 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, andpnpm sbomno longer republishes it as a CycloneDX/SPDX checksum. Lockfiles carrying one also load again instead of failing withERR_PNPM_BROKEN_LOCKFILE#13042.pnpm sbomnow also publishes the checksum of atype: binaryruntime archive, which pnpm does verify.A git dependency whose
git ls-remotefails now reports theERR_PNPM_GIT_RESOLVE_FAILEDcode, naming the dependency instead of printing a baregitinvocation, 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
gitexecutable 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-remoteround-trip instead of two.pnpm installafter moving a dependency betweendependencies,devDependencies, andoptionalDependenciesnow updates the lockfile in place instead of re-resolving the whole dependency graph #13696.syncInjectedDepsAfterScriptsno longer fails withERR_PNPM_UNSUPPORTED_INODE_TYPEwhen a workspace package contains an inode that is neither a file nor a directory, such as the FIFO 1Password's environments create for.env. Such an inode cannot be hardlinked into the injected copy, so it is skipped and the rest of the package still syncs #13550.syncInjectedDepsAfterScriptsalso no longer fails withEEXISTwhen a workspace package replaced a file with a directory of the same name since the injected copy was last synced.syncInjectedDepsAfterScriptsno longer fails withENOTDIRwhen a workspace package replaced a directory with a file of the same name and the injected copy still held that directory's contents.syncInjectedDepsAfterScriptsnow 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.syncInjectedDepsAfterScriptsnow identifies a file by its device as well as its inode number. An inode number is only unique within one filesystem, so on its own it could match an unrelated file on another device and leave that path stale in the injected copy.pnpm store pruneno longer deletes the lockfile verification log. The log records which lockfile passed which supply-chain policies, so it stays valid across a prune of the store; keeping it lets the next install skip re-verifying an unchanged lockfile.Widening a dependency's range no longer leaves the project on an older version. The lockfile update now points the project at the highest version of that dependency already in the lockfile that satisfies the new range — matching what a full resolution records — instead of keeping the locked version whenever it happened to satisfy, which could leave a duplicate behind. A range change that only an already-locked version satisfies is now also handled without re-resolving #13778.
resolutionModeis no longer ignored whenminimumReleaseAgeis in effect.lowest-directandtime-basedpick the lowest satisfying version of a direct dependency again; previously any active release-age cutoff — including the built-in default — silently forced the highest, soresolutionModeonly worked whenminimumReleaseAge: 0was set explicitly #13752.Adding a package to a workspace no longer forces a full re-resolution when every dependency it declares is already locked for a sibling. The lockfile update writes the new project's importer entry from the versions the lockfile already holds; a dependency no locked version satisfies still reaches the resolver #13696.
pnpm config delete <key>no longer fails withENOENTwhen the config file it would edit does not exist. Clearing a setting that was never set is a no-op #13651.Changing a
pnpm.overridesentry to a version range now updates the lockfile in place when a version the lockfile already holds satisfies the range, instead of re-resolving the whole dependency graph. Only exact versions were handled before #13696.Changing a parent-scoped
pnpm.overridesentry ("parent>child": "2.0.0") now updates the lockfile in place instead of re-resolving the whole dependency graph. Only the named parent's dependency moves; every other package keeps the version it had #13795.Removing a dependency, or moving one to another already-locked version, no longer re-resolves the whole dependency graph just because some package resolves a peer with the same name. The lockfile update now compares the peer suffixes against the exact
name@versionthe removal severed, so a suffix that names a different — still present — version of that dependency is left alone #13781.Projects with a pnpmfile now use the fast lockfile update paths: an unchanged pnpmfile (proven by the recorded
pnpmfileChecksum) no longer forces a full re-resolution for removals, dependency group moves, compatible range changes, and the other in-place lockfile rewrites #13696.A lockfile entry whose resolution is unchanged no longer loses its recorded
deprecatedmarker 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 pruneis now recursive by default inside a workspace, just likepnpm install. This fixespnpm prune --prodin a workspace root emptying thenode_modulesdirectories of the other workspace projects, dropping the links to the workspace packages they depend on in production #13718.A setting written in kebab-case in the global
config.yamlis now reported instead of being silently ignored #13650.pnpm removeno longer re-resolves the dependency graph. The removed dependency's entries are dropped frompnpm-lock.yamland anything they made unreachable is pruned, without registry access. The install still falls back to a full resolution when a surviving package resolves a peer dependency through the removed one.Removing a package from a workspace no longer forces a full re-resolution. The lockfile update drops the departed project's importer entry and prunes whatever only it depended on. A project that is still linked from a surviving project continues to be reported as an error #13696.
An install sharing a global virtual store no longer removes an incomplete package directory that another importer is still writing, which could fail with
failed to remove existing directory ... prior to swap: Directory not empty. Such a directory is now repaired in place, and a package file left damaged by an interrupted install is restored instead of being kept.pnpm sbomno 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-onlystill describes the whole lockfile graph, which is platform-independent by design.An
ssh://git dependency pointing at a bracketed IPv6 host, such asssh://[::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 withTypeError: Cannot read properties of undefined (reading 'includes'). Only theuser@hostform worked before.packageExtensionsis now validated when the configuration is read, so a malformed entry (for instance a dependency range set tonull) fails with an actionable error instead of crashing later during peer dependency resolution #13756.Projects using
resolutionMode: time-basednow benefit from the fast lockfile update paths. A removal, a dependency group move, or a compatible range change no longer forces a full re-resolution just because the lockfile carries atimefield #13696.An install that drops the last dependent of a patched package no longer updates the lockfile in place and succeeds silently. Removing a dependency, widening
ignoredOptionalDependencies, or adding a removal override could each prune the package while the patch stayed configured; such an install now falls back to a full resolution, which reports the unused patch withERR_PNPM_UNUSED_PATCH. UnderallowUnusedPatches, where the lockfile update is kept, the same install now warns that the patch went unused instead of saying nothing #13827.Platinum Sponsors
Gold Sponsors
v11.21.0: pnpm 11.21Compare Source
Minor Changes
Added interactive group selection to
pnpm update --global --interactive.Running
pnpm setup,pnpm self-update, or a command that modifies the global installation (such aspnpm add --global) throughsudonow prints a warning. pnpm keeps global packages and configuration in the invoking user's home directory, so running these commands as root silently operates on the root user's home directory instead of yours. They will fail withERR_PNPM_SUDO_NOT_SUPPORTEDin pnpm v12. Read-only global commands (such aspnpm bin --global) are unaffected.Patch Changes
Fixed pnpm failing to start under asynchronous Node.js module loaders when no
.pnpmfile.mjsexists pnpm/pnpm#11701.Fixed
minimumReleaseAgefallback for custom dist-tags so the selected version does not exceed the registry’s original tag target.Removing a dependency from
package.jsonand reinstalling no longer re-resolves the dependency graph. The importer's entry is dropped frompnpm-lock.yaml, anything it made unreachable is pruned, and a catalog entry that loses its last referent is removed — all without registry access. Installs still fall back to a full resolution when a package that stays resolves a peer dependency through the removed one, since that would change the surviving package's entry rather than only prune.Changing a catalog entry to a different exact version no longer re-resolves the dependency graph. The package is replaced in
pnpm-lock.yamldirectly, reusing the same check thepnpm.overridesfast path applies: every locked dependency of the package must still satisfy the new version's manifest. Installs fall back to a full resolution when anything other than the catalog reaches the package — an importer that depends on it directly, or another package that depends on it — since the graph would then need both versions.Fixed a CI regression where
github:owner/repodependencies (and other shorthand Git specifiers) would fail to install withPermission denied (publickey)on CI runners that lack SSH keys. The Git resolver no longer records an SSH URL unless the user explicitly wrote one (e.g.git+ssh://orgit@host:...):429 Too Many Requests, so host throttling of CI runners is no longer mistaken for a private repository.git ls-remoteaccess is now tried before SSH, so a public repository whose visibility probe fails still resolves to a portable HTTPS URL instead of an SSH URL that only works where SSH keys are configured.codeload.github.com, which would fail to download for a private repository); it stays a regulargitresolution so installs can use ambient Git credentials such as credential helpers and tokens.Note that a private repository that is reachable both over authenticated HTTPS and over SSH now resolves to its HTTPS URL, where previous versions recorded the SSH URL.
Fixes pnpm/pnpm#13276.
ng buildandnuxt buildnow work under the global virtual store: pnpm's built-in compatibility extensions add thetslibdependency that@angular/builduses without declaring and theunplugindependency that@nuxt/vite-builderv4 uses without declaring.Fixed
link:dependencies underenableGlobalVirtualStoreso linked children are materialized and slots remain isolated by their resolved link targets.An install that skips resolution because
pnpm-lock.yamlis already up to date now reacts fully to packages the lockfile removed — for example after pulling a lockfile in which a dependency was deleted. The hoist layer is recomputed, so a package that became hoistable when a direct dependency was removed is hoisted, andpendingBuildsentries for removed packages are dropped instead of staying pending forever.The held-back-update warning printed by
pnpm updateno longer fires whenminimumReleaseAgeis 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 #13071.Checking whether
ignoredOptionalDependenciesis up to date no longer reorders the configured patterns. The check sorted them in place, which could move an!exclusion ahead of the pattern it excludes from and flip which optional dependencies were ignored.Changing
autoInstallPeers,dedupePeers,peersSuffixMaxLength,excludeLinksFromLockfile, orinjectWorkspacePackagesno 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 inpnpm-lock.yamland the install proceeds from the existing resolution. Every other case still falls back to a full resolution.Adding, editing, or removing an entry in
patchedDependenciesno 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 inpnpm-lock.yamland 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 whileallowUnusedPatchesis off, soERR_PNPM_UNUSED_PATCHis still reported.Resolving a private git repository no longer blocks on an interactive credential prompt:
git ls-remotenow fails fast with an authentication error when git has no credentials for the repository #13522.Lockfile verification now honors offline mode by using cached registry metadata instead of reaching the registry. When the required metadata is not available locally, verification reports the same
ERR_PNPM_NO_OFFLINE_METAcondition 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 onPATH#13405.The automatic
packageManagerversion 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 automaticpackageManagerversion switch andpnpm self-update#13147. When the configured registry cannot provide a verifiable signature, pnpm now fetches the signature fromregistry.npmjs.organd 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 ashasum), pnpm proceeds with a warning instead of failing, but only when the packages resolve through a registry configured in the user's own (non-project) configuration; the download stays pinned by the lockfile integrity, and a signature that exists but does not validate still fails the switch.pnpm fetch, and any install run withvirtualStoreOnly, no longer writes a.pnp.cjsloader undernodeLinker: 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 andnode_modules/.package-map.jsonwere already skipped; the PnP loader now follows the same rule.Prevent pnpm from removing project files when
modulesDirresolves to the project root.Speed up installs after adding
ignoredOptionalDependenciespatterns by removing newly ignored optional dependencies and pruning packages that are no longer reachable without resolving the dependency graph again.When a failed install re-copies a bin script from the store, rerunning
pnpm installnow reapplies the executable bit to the bin instead of leaving it non-executable #12742.pnpm root -gandpnpm bin -gnow print warnings to stderr instead of stdout, so their stdout stays a clean, machine-readable path. Previously, running either command with--globalin a project that pins a package manager (e.g. via thepackageManagerfield) printed a warning like[WARN] Using --global skips the package manager check for this projectahead of the path, breaking programs that capture the output as a path #13672.In pnpm 12,
pnpm root -gandpnpm prefix -gare now supported (they previously failed withERR_PNPM_CLI_ROOT_GLOBAL_UNSUPPORTED/ERR_PNPM_CLI_PREFIX_GLOBAL_UNSUPPORTED), and the reporter output ofdlx,create,config,sbom,with,store,prefix,root, andbingoes to stderr, matching pnpm 11.pnpm setupno longer makes Node.js print aMODULE_TYPELESS_PACKAGE_JSONwarning aboutdist/worker.json every command. Thepackage.jsonit writes next to a standalone executable now declares"type": "module".pnpm updatewithout 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--latestresolves within it instead of past it. Previously each of these could write a lockfile entry that contradicted its own specifier, which the nextpnpm install --frozen-lockfilerejected withERR_PNPM_OUTDATED_LOCKFILE#12764.pnpm version -r --jsonnow outputs[]instead of human-readable text when no pending changes existpnpm/pnpm#13217.Platinum Sponsors
Gold Sponsors
v11.20.0: pnpm 11.20Compare Source
Minor Changes
Security fix. Affects projects using
namedRegistrieson 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@versionalone, and entry lookup went throughrefToRelative(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 singlepackages: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
namedRegistriessees no difference, and older pnpm versions keep reading the file.If you use named registries
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
namedRegistriessetting on demand.To use named registries, map your aliases in
pnpm-workspace.yaml:New built-in
npmjs:aliasnpmjs:now resolves tohttps://registry.npmjs.org/with no configuration, alongside the existinggh:alias for GitHub Packages. It pins a dependency to the public registry even whenregistrypoints elsewhere, such as an internal proxy:npm:cannot do this — it is the alias protocol (npm:<name>@<range>) and resolves through whateverregistrypoints at.If you mirror or proxy npmjs, point the alias at your mirror:
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.orgis 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, ortrustPolicycheck runs. Overriding the alias is the same escape hatch GHES users already have forgh.Every alias the lockfile references must stay in
namedRegistries: reading an entry whose alias is gone fails withERR_PNPM_MISSING_NAMED_REGISTRYrather 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 withERR_PNPM_RESERVED_NAMED_REGISTRY_NAMEinstead of being silently shadowed by the corresponding resolver.pnpm licensesandpnpm sbomnow keep the two artifacts apart as well: license records carry the registry alias, and SBOM components carry the purlrepository_urlqualifier.Patch Changes
An empty
http-proxy,https-proxy,proxy, orno-proxyvalue — from the.npmrc,pnpm-workspace.yaml, the CLI, or theHTTP_PROXY/HTTPS_PROXY/PROXY/NO_PROXYenvironment variables — no longer fails the install withERR_PNPM_INVALID_PROXY. Empty settings read as unset, so a shell exportingHTTP_PROXY=disables the proxy, and an emptyproxy=in the.npmrcno longer suppressesHTTPS_PROXY#13533.proxy=falsein the.npmrcorproxy: falseinpnpm-workspace.yamlnow turns proxying off instead of being read as a proxy host namedfalse.falseandnullonhttps-proxy/http-proxy/no-proxyread 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/exealongsidepnpmwhen the wanted pnpm version is 12 or newer. From v12 the unscopedpnpmpackage is itself the native executable, so@pnpm/exeis not published for it and resolving it would fail. The engine identity check now verifies the native binary through whichever package ships it.lexCompareandnerfDartare now published as@pnpm/text.ordinal-comparatorand@pnpm/config.registry-auth-key. Use these instead of@pnpm/util.lex-comparatorand@pnpm/config.nerf-dart.Fixed the order in which pnpm matches a lockfile's recorded tarball URL against known registry URLs. Two registry URLs of equal length were previously ordered arbitrarily, so which one a tarball URL matched could differ between runs.
Dependency resolution is faster: package metadata is now filtered once per packument instead of once per dependency edge when
minimumReleaseAgeis active, and parsed semver versions and ranges are reused instead of re-parsed on every comparison.Security:
pnpm rebuildnow refuses a lockfile whosepackageskey carries a path traversal in the package name (e.g.../../../escaped@1.0.0), instead of running that package's lifecycle scripts and linking its bins in a directory outside the virtual store. Such a name is rejected withERR_PNPM_INVALID_DEPENDENCY_NAME.Platinum Sponsors
Gold Sponsors
v11.19.0: pnpm 11.19Compare Source
Minor Changes
pnpm loginno longer requires an interactive terminal when the registry supports web-based login: without a TTY it prints the authentication URL (skipping the QR code and the "Press ENTER to open the URL in your browser" prompt) and polls the registry until the browser approval completes. Only the classic username/password login still fails withERR_PNPM_LOGIN_NON_INTERACTIVEin a non-interactive terminal.The
save-prefixsetting now accepts=: newly added dependencies are saved with an explicit=operator (=1.2.3) instead of the setting being silently treated as the default^.Patch Changes
allowBuildsentries can now approve git-hosted packages that pnpm downloads as a tarball, such asgithub:dependencies (which are fetched fromcodeload.github.comrather than cloned), by their repository URL without the resolved commit hash. This matches the hashlessgit+matching already supported for cloned git dependencies. For example:This approves the package whether pnpm clones it or downloads a tarball, so the entry no longer has to be updated every time the pinned commit changes. GitLab and Bitbucket tarball downloads are matched the same way. Approving or denying a specific resolved commit by its full tarball dep path continues to work.
pnpm outdated --include-github-actionsno longer blocks on an interactive git credential prompt when a workflow uses a private action repo.Prevented
minimumReleaseAgefrom replacinglatestwith a SemVer-greater version than the registry tag target #13034.Fixed empty
bundledDependenciesandbundleDependenciesarrays causing nondeterministic lockfile changes. See #13123.The install summary no longer prints
(X is available)when the registry'sdist-tags.latestis still held back by the activeminimumReleaseAgepolicy. The hint only ever names the actual latest tag, so an immature latest suppresses the hint instead of advertising the version pnpm just refused to install #11698.pnpm updatekeeps the explicit=operator of an exact version pin: a dependency saved as=3.5.1now updates to=3.5.2instead of the bare3.5.2. See #13168.Preserve a workspace dependency's
link:entry when a run does not target it — e.g.pnpm update <other-pkg>(with or without--recursive), or a plain install after a root/catalog dependency change — withinjectWorkspacePackages, instead of spuriously rewriting it to a peer-suffixedfile:protocol. See #10433.Workspace dependencies declared with a relative path (e.g.
"foo": "workspace:../foo") are no longer silently dropped from the workspace projects graph, so--filterselection and the topological order of recursive commands take them into account.Platinum Sponsors
Gold Sponsors
v11.18.0: pnpm 11.18Compare Source
Minor Changes
Fixed an installed optional dependency being left without one of its own required dependencies. When a package reached through
optionalDependenciesis installable on the current system but one of its regulardependenciesis not, a lockfile-based install skipped that dependency and installed the parent anyway, so importing the parent failed withMODULE_NOT_FOUND. The dependency is now installed, and an install-check warning reports the incompatibility. A dependency is still only skipped when every path to it is optional, or when the package that pulls it in was itself skipped #13286.pnpm setupnow appendsPNPM_HOMEand the global bin directory to the GitHub Actions environment files (GITHUB_ENVandGITHUB_PATH), so later steps in the same job can runpnpm add --globaland other global commands #9191.Added support for
publishConfig.name, which publishes a package under a different name than the one its manifest carries in the workspace. It is for a project whose published name is already taken by a sibling project, which otherwise has to be renamed by a build step just before publishing. Only the published artifact is renamed — dependents,pnpm-lock.yaml, and release tooling keep addressing the project by its manifest name — and the new name reaches the packed manifest, the tarball filename, and everything that addresses the package at the registry: the already-published check ofpnpm publish -r, its registry selection, and the release-planning probes ofpnpm change statusandpnpm version -r#13345.pnpm self-updateno longer takes any instruction from the project it is run in:.npmrcorpnpm-workspace.yamlcan no longer redirect the download or attach credentials to it, and the project's default.pnpmfile.(c|m)jsis no longer loaded. Pnpmfiles from trusted sources (thepnpmfilesetting, the global pnpmfile, config dependencies) still apply.minimumReleaseAgesettings inpnpm-workspace.yamlno longer affectself-update. They still govern the project's own dependencies; forself-updatethe cooldown now comes from the built-in default, your global config, aPNPM_CONFIG_*environment variable, or a command-line flag. This fixesself-updatefailing inside a workspace that raises the cutoff while succeeding everywhere else, and stops a repository from either waiving the cooldown or keeping you on an outdated pnpm by raising it.trustPolicysettings and toci: a project can no longer weaken the trust check that guards the pnpm download, nor re-enable the confirmation prompt that a CI run suppresses.When
self-updaterefuses a version that is younger than the cutoff, an interactive run now offers to update anyway; non-interactive runs still fail. CI never prompts, even on a runner that attaches a TTY.Patch Changes
Fixed
pnpm licenses listto report every version when the same package is installed under multiple aliases pnpm/pnpm#13438.Sort
pnpm dedupe --checksnapshot changes for stable output across pnpm implementations.Strip Unicode formatting characters from registry- and manifest-derived terminal output.
Speed up installs after compatible catalog or direct dependency range changes by retaining the locked version without resolving the dependency graph again.
Speed up installs after safe override changes by reusing unambiguous compatible dependency resolutions, pruning obsolete dependencies, applying independent replacements and removals together, and handling parent-scoped
"-"overrides without full lockfile resolution.Installing a local
file:directory dependency with the global virtual store enabled no longer fails withTypeError: Cannot read properties of undefined (reading 'split')#13335.Local directory dependencies —
file:directories and injected workspace packages — now get a global-virtual-store slot of their own per project. They used to share one slot across every project that depended on a directory of the same name, so a project could end up linked to another project's copy of the dependency.The
Workspacecolumn ofpnpm update --interactivenow falls back to the project's path when itsnameis only whitespace, as it already did for a missing or empty one — all three render an equally blank label otherwise.Checking GitHub Actions dependencies for updates is now opt-in for every command. Neither
pnpm outdatednorpnpm updatereads the workflow files unless--include-github-actionsis passed orupdate.githubActionsis set totrueinpnpm-workspace.yaml. Reading them runsgit ls-remoteagainst every referenced repository, which fails in environments where GitHub is not reachable the way pnpm assumes (a GitHub Enterprise Server, a custom certificate authority, or an offline network) #13254.pnpm outdatedaccepts the--include-github-actionsoption too.pnpm update --interactivenow measures its table in terminal columns rather than in characters. A package name, workspace name, or version containing wide characters (CJK, most emoji) no longer knocks its row's columns out of line with the rest of the group, and a wide character in a version no longer aborts the command withSubject parameter value width cannot be greater than the container width#13357.The
Workspacecolumn ofpnpm update --interactiveis more informative in two cases. A dependency outdated at the same version in several workspace projects is offered as one choice, since selecting it updates every project — that choice now names all of them instead of only the first. And a workspace project without anameis now labelled with its path rather than left blank, so several unnamed projects can be told apart.An auto-installed optional peer is no longer hoisted at a version the workspace root's own dependency on that package excludes.
resolvePeersFromWorkspaceRootalready made the workspace root's specifier decide which version a missing required peer is installed at; the optional-peer picker ignored it and always took the highest version present anywhere in the graph. In a workspace whose root pinspostcss: 8.5.10, an importer that depends onwebpackand declares nopostcssof its own gotpostcss@8.5.22hoisted forterser-webpack-plugin's optionalpostcsspeer, leaving twopostcss@8.5.xinstances in the graph #13320.overridesnow also govern peers that pnpm auto-installs. Previously an override only rewrote dependencies declared in a manifest, so a peer nobody declares — installed becauseautoInstallPeersis on — resolved against its declared peer range and could bring in a second copy of the very package the override pinned. For example, withoverrides: { react: npm:react@19.2.0 }and a lonelucide-reactdependency, pnpm installedreact@18.3.1; it now installs the pinnedreact@19.2.0#13320.Under
resolvePeersFromWorkspaceRoot, a workspace root dependency declared withlink:orfile:(or the path form ofworkspace:, such asworkspace:../pkg) now satisfies another project's missing peer dependency at the linked package's own version, instead of being hoisted as a path. Those specifiers are relative to the project that declares them, so the same specifier reached a different directory — or none — from the project the peer was hoisted into, leaving a broken link. The root now has the same authority over the peer as it has when it declares the package with a version range #13373.Installs through a pnpr server now apply the project's whole verification policy.
minimumReleaseAgeExclude,minimumReleaseAgeIgnoreMissingTime,trustPolicy,trustPolicyExclude,trustPolicyIgnoreAfter, andtrustLockfilewere ignored, so excluded packages were still held back and a lockfile containing them could be rejected.trustPolicy: no-downgradeno longer fails withTRUST_POLICY_INCOMPATIBLE_WITH_PNPRwhen a pnpr server is configured.--frozen-lockfileand--no-prefer-frozen-lockfileare now honored on the pnpr path, instead of resolving and rewriting the lockfile anyway. SincefrozenLockfiledefaults totrueon CI, a CI install through a pnpr server now fails on an out-of-date lockfile rather than updating it.Workspace installs through a pnpr server no longer crash with
Cannot read properties of undefined (reading 'filter')after linking, whenminimumReleaseAgeis active #13275.Fixed
pnpm dedupeupdating valid catalog resolutions when another matching version exists in the lockfile.pnpm -r run "/pattern/" --no-bailno longer exits zero when one of a project's matched scripts fails and a later one passes. The run summary carries a single status per project, and the passing script overwrote the recorded failure.Restored the store block a first install prints, naming how packages were materialized and where the stores live #13315:
The root project's
pnpm:devPreinstallscript now runs before resolution and linking, as it does in pnpm 11. It is skipped under--ignore-scripts,--lockfile-onlyand--dry-run, bypnpm fetchandpnpm rebuild, and by a repeat install that is already up to date. Workspaces that use the hook to prepare state the install depends on — such as next.js, which generates a placeholdernextbin with it — were left with dependents linked against files that were never created #13313.Prevented
pnpm dedupe --checkfrom removing an incompatiblenode_modulesdirectory.pnpm update --workspaceno longer links dependencies the user never named:updateConfig.ignoreDependenciesconfigured no longer fails withERR_PNPM_WORKSPACE_PACKAGE_NOT_FOUNDfor a dependency that is only published to the registry. Such dependencies keep their specifiers, as they already did when no dependencies were ignored.Platinum Sponsors
Gold Sponsors
v11.17.0: pnpm 11.17Compare Source
Minor Changes
Added a new setting,
update.githubActionsServer, for specifying the base URL of the GitHub server that hosts the repositories of the GitHub Actions referenced by the workflow files (for example, a GitHub Enterprise Server). When the setting is not defined, the URL is read from theGITHUB_SERVER_URLenvironment variable, falling back tohttps://github.com. The URL must use thehttps://orhttp://protocol #13220.pnpm outdatedandpnpm updateno longer fail when the refs of a GitHub Action's repository cannot be read (for example, when the action's repository is private or hosted on a different GitHub server). Such actions are now skipped with a warning.Setting
update.githubActionstofalsenow makespnpm outdatedand the interactivepnpm updateskip GitHub Actions dependencies.Patch Changes
The token poll for web-based authentication no longer reads the body of non-OK or still-pending (HTTP 202) responses, and caps the token response body it does read at 64 KiB, so a malicious or compromised registry cannot exhaust memory through the poll pnpm/pnpm#12721.
Fixed
catalog:references in dependencies and overrides failing to resolve when installing through a pnpr server, which errored with "No catalog entry '' was found for catalog 'default'." even though the catalog entry existed. Also fixed a crash on Windows when installing a nested workspace member (e.g.packages/foo) through a pnpr server #13232.Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug #13164.
Revert script ordering change for
pnpm run --sequential /regex/Support the
from-gitargument in thepnpm versioncommand.When the authentication URL cannot be rendered as a QR code (for example when it exceeds the maximum QR data capacity), web-based login now displays the URL alone with a warning instead of aborting authentication pnpm/pnpm#12721.
Platinum Sponsors
Gold Sponsors
v11.16.0: pnpm 11.16Compare Source
Minor Changes
The first release of a package now publishes the version written in its manifest verbatim, instead of bumping off it.
pnpm version -randpnpm change statuscheck the registry for each release's current version; when that version is not yet published, the package debuts at it and its pending changesets apply only from the next release. A newly added package seeded at1100.0.0with aminorchangeset is therefore published as1100.0.0rather than skipping straight to1100.1.0.Added a
--changesetflag topnpm update. Setupdate.changesettotrueinpnpm-workspace.yamlto enable this behavior by default, and use--no-changesetto override the setting for one update. After the update completes, pnpm writes a.changeset/pnpm-update-<suffix>.mdfile declaring a patch bump for every workspace package whosedependenciesoroptionalDependencieswere changed by the update and a major bump whenpeerDependencieschanged, including packages that consume an updated catalog entry via thecatalog:protocol. Private packages, packages without a name, and packages listed in theignorearray of.changeset/config.jsonare skipped. If.changeset/config.jsondoes not exist, a warning is printed and no changeset is generated.Added GitHub Actions dependencies to
pnpm outdatedand interactivepnpm update. Non-interactive updates can include them with--include-github-actionsor by settingupdate.githubActionstotrueinpnpm-workspace.yaml. Updated actions are pinned to exact commit hashes with their release tags preserved in comments.Added
updateandauditsettings sections topnpm-workspace.yaml, superseding the awkwardly namedupdateConfig,auditConfig, and top-levelauditLevelsettings:update.ignoreDepslists dependency name patterns thatpnpm updateandpnpm outdatedshould skip.audit.levelandaudit.ignoretunepnpm audit.The deprecated
updateConfig,auditConfig, andauditLevelsettings keep working until the next major version. When both a new section value and its deprecated counterpart are set, the new section takes precedence and a warning is printed. Both the TypeScript CLI and the Rust config surface (pacquet) recognize the new sections.Patch Changes
pnpm add --save-exact/--save-prefixandpnpm updatewriting a package's version with thepeerDependenciesrange's prefix (e.g.^19.2.7instead of the requested19.2.7) whenever the same package also appeared inpeerDependencies. A realdependencies/devDependencies/optionalDependenciesentry now takes precedence over a same-namedpeerDependenciesentry when computing the current specifiers #13108.Platinum Sponsors
Gold Sponsors
v11.15.1: pnpm 11.15.1Compare Source
Patch Changes
pnpm installnow detects asupportedArchitectureschange and re-evaluates previously skipped platform-specific optional dependencies, instead of reporting the project as up to date and leaving the packages for the old architecture set in place.pnpm setupnow removes leftover v10-layout shims at the top ofPNPM_HOME, sopnpm self-updateno longer warns about a v10 installation layout after PATH has been migrated to the v11PNPM_HOME/binlayout. Applies to both the TypeScript CLI and pacquet.In the TypeScript CLI,
self-updatealso no longer treats a dangling legacy shim (one whose install target was garbage-collected) as a real v10 layout, so the warning can no longer fire on dead shim files.Closes #12496.
Completed pnpm runtime installation parity for Node.js, Deno, and Bun, including runtime failure policy, target architecture selection, and dependency runtime engines. Runtime failure overrides now preserve explicit runtime dependencies without matching engine entries.
Fixed
pnpm installrunning out of memory while resolving large dependency graphs #8441. The resolver kept full registry documents — per-version readmes, scripts, descriptions, and other install-irrelevant bulk — in memory for every package fetched with full metadata (optional dependencies, and packages re-fetched forminimumReleaseAge's publish timestamps). Every retained document is now condensed down to the field set installation actually reads, which reduces peak resolution memory by several times on workspaces with more than a thousand packages.When a dependency's build script fails under
enableGlobalVirtualStore, the global virtual store directory it was being built in is now removed for scoped packages too. Previously the cleanup resolved one directory level short of the hash directory for a scoped name, leaving a half-built directory behind that later installs would reuse.Fixed
pnpm login,pnpm adduser, andpnpm logoutagainst a registry hosted under a URL subpath (e.g.https://example.com/npm/registry) when the configured URL has no trailing slash. Such URLs were left unnormalized, so the last path segment was dropped when building the login and token endpoints and the auth token was stored under a truncated key. Registry URLs with a path now always get a trailing slash appended during normalization, matching how root-level registry URLs are handled.Platinum Sponsors
Gold Sponsors
v11.15.0: pnpm 11.15Compare Source
Minor Changes
peerDependenciesMeta(for exampledebug'ssupports-colorpeer) are now resolved from a satisfying version already present in the dependency graph, the same way explicitly declared optional peer dependencies are. Previously such peers were only resolved this way when the package's metadata was read back from the lockfile, so an unrelated dependency change could rewrite peer resolutions across the whole lockfile.Patch Changes
Updated
adm-zipto prevent crafted ZIP archives from causing excessive memory allocation.pnpm version -rno longer writes a versioning-ledger entry with no consumed intents as a bareintents:key, which the next run failed to read withERR_PNPM_INVALID_VERSIONING_LEDGER. Empty intent lists are now written asintents: [], and the ledger reader accepts the bare form left by earlier releases.Fixed pnpr workspace resolution to preserve project names and versions for
workspace:dependencies.Platinum Sponsors
Gold Sponsors
v11.14.0: pnpm 11.14Compare Source
Minor Changes
peerDependenciesnow accept dependency specifiers that carry a scheme — a named-registry spec (<registry>:<version>), annpm:alias, or afile:/git/URL spec — instead of rejecting them withERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION#13095. Such a peer is matched against the semver range carried by the specifier (work:5.x.xis checked as5.x.x,npm:bar@^5as^5), or against*when it carries no version, while the original specifier still selects the package to auto-install. Barename@versionvalues, which are almost always a mistake, are still rejected.Added
pnpm doctor, which diagnoses the pnpm installation and the environment it runs in: the versions and install method, whether the global bin directory is onPATH, whether the store and cache are writable, which link strategies (reflink, hardlink, symlink) the store's filesystem supports, registry connectivity, and an offlinefile:install that exercises the resolve/store/link path end to end. Each check reports how to fix what it finds, and the command exits non-zero when any check fails.Use
--offlineto skip the checks that need network access,--jsonfor machine-readable output, and--benchmarkto time the filesystem and install checks.Added support for executing multiple scripts matching a RegExp passed to
pnpm run(e.g.,pnpm run "/^build:.*/"), running matched scripts in deterministic lexicographical order. Restored the--sequential(-s) CLI option forpnpm run, which forcesworkspaceConcurrencyto 1 so that matched scripts run sequentially one by one across and within packages.Patch Changes
Fixed
pnpm installfailing withERR_PNPM_LOCKFILE_IS_SYMLINKwhenpnpm-lock.yamlis a symlink, as build sandboxes such as Bazel and Nix stage it #13073. Reading a lockfile through a symlink is allowed again, and an install that leaves the lockfile unchanged no longer rewrites it, so--frozen-lockfileno longer needs to write at all. Writing a changed lockfile through a symlink is still refused, as that would redirect the write onto the symlink's target.Fixed frozen installs incorrectly treating equivalent Git dependency specifiers as a stale lockfile. See #13039.
pnpm owner lsnow reports authentication and authorization failures (401/403) as dedicated errors that include the registry's response body, matchingpnpm owner add/rm, instead of a genericFailed to fetch ownersmessage.Recover from a metadata cache entry that disappears (concurrent cache cleanup, antivirus) after the registry has already answered the conditional request with
304 Not Modified. The metadata is re-requested once without cache validators instead of failing the install withERR_PNPM_CACHE_MISSING_AFTER_304.A project pinned to a broken pnpm release via
packageManagerordevEngines.packageManagernow reports which release is broken and what to do about it, instead of failing inside the installer.pnpm self-updatealready refused these releases; the version switch does too.Prevent broken-lockfile errors from including snippets of the lockfile's contents.
pnpm self-updatenow checks that the version it installed can run before making it the active pnpm. A release that installs but cannot execute is discarded with an error instead of replacing a working installation.Fixed an out-of-memory regression when workspace projects concurrently resolve a package with large registry metadata pnpm/pnpm#13077.
Fixed
pnpm updaterewriting exact version pins that use the=operator (for example=3.5.1) to a caret range (^3.5.1). Exact pins are now preserved and written back as the bare version. See #12745.Platinum Sponsors
Gold Sponsors
v11.13.1: pnpm 11.13.1Compare Source
Patch Changes
pnpm packapplying workspace-root ignore rules when a workspace package has its own.npmignorefile.minimumReleaseAgeapproval prompt visible duringpnpm install. The progress reporter now pauses its redraws while a prompt is waiting for input instead of overwriting it, so the install no longer hangs on a question the user cannot see #13019.pnpm self-updatefailing to link native platform binaries stored in sibling global virtual store slots.v11.13.0: pnpm 11.13Compare Source
Minor Changes
Added
versioning.epicstopnpm-workspace.yaml. An epic ties a group of member packages to a lead package, constraining every member's major version to a band derived from the lead's major: while the lead is on majorM, members live inM*100 … M*100+99. Members move independently inside the band (patch, minor, and amajorintent that stays in-band); a bump that would carry a member past the band ceiling is rejected until the lead advances its own major. When a release plan takes the lead to a new stable major, every member re-bases to the band floor in the same plan. Membership is matched with pnpm's package selectors — name globs,./-prefixed directory globs, and!-prefixed negations.Added the
teamcommand for managing organization teams and team memberships on the registry, with create, destroy, add, rm, and ls subcommands and support for --otp, --parseable, and --json flags.Added native workspace release management #12952: the new
pnpm changecommand records change intents as changesets-compatible.changeset/*.mdfiles (pnpm change statusshows the pending release plan), and the barepnpm version -rconsumes them — bumping versions across the workspace with dependent propagation throughworkspace:ranges, fixed groups, amaxBumpcap,--filternarrowing, and--dry-run— writing changelogs, and recording consumed intents in a committed ledger that keeps cherry-picks and merge-backs between release branches safe. Packages can be moved onto per-package release lanes with the newpnpm lane <name> --filter <pkg>command and back withpnpm lane main --filter <pkg>(pnpm laneshows the membership), releasingX.Y.Z-lane.Nprereleases from the same runs that release stable versions of the packages on the main lane. Configuration lives under the newversioningkey ofpnpm-workspace.yaml(fixed,ignore,maxBump,lanes,changelog). When two workspace projects publish the same name, intent files,versioning.lanes, andversioning.fixed/ignoremay reference a project by its workspace-relative directory path (e.g."./pnpm/npm/pnpm") — the one additive extension to the changesets format, applied automatically bypnpm change.Release changelogs default to
registrystorage (versioning.changelog.storage): noCHANGELOG.mdis committed. Each release's section is composed at publish time and packed into the published tarball on top of the previously published version's changelog, and the consumed change intents are garbage-collected by a laterpnpm version -ronly once the registry confirms the version is published with its section. Setversioning.changelog.storage: repositoryto keep committedCHANGELOG.mdfiles instead.Added a new override selector form with an empty range —
"pkg@": "<version>"— called a convergence override. It rewrites a dependency edge only when its exact version satisfies the edge's declared range, so compatible consumers converge on one version while incompatible consumers keep their own resolution — now and for any dependent added in the future #12794.The value must be an exact version. When a full resolution detects that every declared range also admits a newer version, pnpm warns that the override is stale and names the version to converge on. Previously an empty range in an override selector was undocumented and behaved like a bare (unscoped) override.
Patch Changes
A
tokenHelperset in the global pnpmauth.iniis no longer rejected as project-level configuration. The guard that blockstokenHelperfrom a project.npmrconly treated~/.npmrcas a trusted source, so a helper written toauth.ini(for example bypnpm config set) failed on every command and could not even be removed withpnpm config delete. AtokenHelperin a workspace or project.npmrcis still rejected.pnpm cache deletenow removes a package's metadata from every metadata cache directory (metadata,metadata-full, andmetadata-full-filtered), instead of only the one the current resolution mode reads. Previously a package cached under a different mode (e.g.metadata-full-filtered) was left behind. Closes #12753.Fixed an injected workspace dependency (
injectWorkspacePackages: true) incorrectly staying asfile:instead of deduping back tolink:when an unrelated, ordinary shared dependency resolved to a peer-suffixed variant for the target project's own copy but not for the injected occurrence. See #10433.pnpm deploynow supports workspaces that use catalogs.Fixed
pnpm deploywith a shared lockfile so localfile:tarball dependencies keep their package name in the generated deploy lockfile. This prevents warm-store deploys from failing withERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STOREwhen the tarball filename includes the version.Options that follow
create,exec, ortestappearing as a subcommand of another command are now parsed instead of being silently treated as positional parameters. For example,pnpm team create @org:team --registry <url>previously ignored the--registryoption and sent the request to the default registry.pnpm add -g,pnpm update -g,pnpm setup, and the self-updater no longer fail withERR_PNPM_MISSING_TIMEwhentrustPolicy: no-downgradeorresolutionMode: time-basedis set in the global config #12883. The decision to fetch full registry metadata now lives in one place, and theno-downgradetrust policy always requests full metadata (matching the self-updater), since the trust evidence it checks is missing from abbreviated metadata even on registries that include thetimefield.pnpm listandpnpm whyno longer crash withEMFILE: too many open fileswhen a project has a large number of unsaved dependencies (packages present innode_modulesbut not in the lockfile). The reads of those packages are now concurrency-limited.The published
pnpmpackage no longer declaresdependenciesordevDependencies. Because the CLI bundles its runtime dependencies intodist/node_modules, those fields are dropped when packing, sonpm installof the tarball no longer tries to resolve internal-only packages such as@pnpm/test-ipc-server. Closes #12955.Fixed
pnpm publish --otpandpnpm publish --batch --otpto send the configured OTP to the registry.pnpm publishagain sends the package's README to the registry as metadata, so registries can render it on the package page. The readme is always included in the published metadata (matching the npm CLI), while theembed-readmesetting continues to control only whether the readme is written into thepackage.jsoninside the tarball. This restores the behavior that was lost when publishing became fully native. Closes #12966.Fixed the dependency status check wrongly reporting "up to date" when a
package.json,.pnpmfile.cjs, or patch file was edited in the same second as the previous install, on filesystems that record mtimes at whole-second resolution (for example ext4 with 128-byte inodes). The optimistic repeat-install fast path andverify-deps-before-runcompared mtimes strictly, so a same-second edit whose mtime rounded down looked unchanged and re-resolution was skipped. Such a file's whole second is now treated as possibly-modified, falling through to the content check; behavior on sub-second filesystems is unchanged.Retry package metadata requests when a registry or proxy returns
304 Not Modifiedto an unconditional request, preventing falseERR_PNPM_CACHE_MISSING_AFTER_304failures pnpm/pnpm#12882.If the retry also returns
304, reportERR_PNPM_META_NOT_MODIFIED_WITHOUT_CACHEinstead.Fixed
pnpm updateremoving transitive lockfile entries whendedupePeerDependentsis disabled and the selected package is absent pnpm/pnpm#12456.Limit modern deploy lockfiles and localized virtual stores to dependencies reachable from the selected dependency groups.
A
tokenHelpercommand is now given a 60-second time limit. A helper that hangs (deadlock, stuck I/O) is killed and reported as an error instead of leaving the command waiting forever.Fixed orphaned child processes on Windows when pnpm exits on an error while commands spawned by
pnpm execorpnpm dlxare still running (for example, when one project's command fails duringpnpm --recursive exec). The PIDs of these commands are now recorded when they are spawned and their whole process trees are terminated withtaskkillon an error exit. Previously the cleanup relied on enumerating the system process list, which is so slow on Windows that the enumeration hit its timeout and the cleanup was silently skipped #12406.pnpm packnow respects workspace-root.npmignoreand.gitignorefiles when packing workspace packages.Platinum Sponsors
Gold Sponsors
v11.12.0: pnpm 11.12Compare Source
Minor Changes
a897ef7: Custom fetchers exported from a pnpmfile can now delegate by returning a{ delegate: <resolution> }envelope: pnpm rewrites the package's resolution to the delegated shape and runs the built-in fetcher on it. This is the portable delegation form that also works in pacquet, wherecafsandfetcherscannot be passed to the hook. Related to pnpm/pnpm#11685.Patch Changes
2b02764: The changed-packages filter (--filter "...[<since>]") no longer allows an option-like<since>value (such as--output=<path>) to be interpreted as a git option — git now rejects it as a bad revision. The repository root is also resolved to the nearest.gitentry, so the filter works in a git worktree checked out inside another repository's tree.43711ce:pnpm outdatedno longer checks the registry for dependencies that are resolved from locallink:,file:, orworkspace:references in the lockfile #12827.3c6718b: Fixed a deadlock in peer dependency resolution:pnpm installhung forever when a peer dependency cycle spanned a project's own dependencies and auto-installed peer providers, for example when installingelectron-builder@26.15.3#12921.252f15e: Fixed peer dependency auto-install picking a version the peer range rejects. In a workspace with several projects, a package declaring a peer dependency with a semver range (for example^1.0.0) could get the highest version found anywhere in the workspace (for example a2.0.0resolved for another project) instead of a version that satisfies the range. Peers are now deduplicated onto the highest preferred version that satisfies the declared range, and when none does, the range is resolved from the registry.Also fixed re-resolving with an existing lockfile hoisting a different peer version than a fresh install of the same manifest: root dependencies reused from the lockfile were invisible to peer hoisting, so a peer that a root dependency provides could be bound to another version.
a38adda:pnpm self-update <version>now installs the requested pnpm version when it matches the currently running version but is missing from the global self-update directory.6a85968:pnpm stage listnow stops paginating after a fail-safe cap of 1000 pages, so a misbehaving registry cannot keep the command looping forever.eee7c9a:verify-deps-before-runno longer spawns apnpm installwhen pnpm is executed in a directory that has nopackage.json. A mistyped command run outside a project (for examplepnpm witch 10 login) used to crash with a confusing error from the spawned install; now it fails with the regular "no package.json found" error.Platinum Sponsors
Gold Sponsors
v11.11.0: pnpm 11.11Compare Source
Minor Changes
508b8c2: Added thepnpm accesscommand for managing package access and visibility on the registry, supporting listing packages and collaborators, getting and setting package status and MFA requirements, and granting or revoking team access.Patch Changes
c70e33e: AllowallowBuildsentries for git-hosted packages to match by repository URL without pinning the resolved commit hash. This lets trusted git repositories keep running their build scripts after branch updates without approving each new commit, while package-name-only rules still do not approve git-hosted artifacts.3067e4f: Reduced peak memory usage during cold-cache dependency resolution. The metadata fetch is memoized for the whole resolution phase, and it was retaining each package's raw registry response body (used only to mirror the response to disk) for that entire time. The memoized cache now holds a body-less copy, so the raw body only lives as long as the call that writes the disk mirror. On large graphs that fetch full metadata (e.g. withminimumReleaseAgeortrustPolicyenabled) this cuts peak RSS by roughly 30%, back in line with pnpm 10. The resolved lockfile is unchanged.51300fd: Prevent a craftedpnpm-lock.yamlfrom writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g.../../../tmp/x@1.0.0) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshotversion: "../../x") is now rejected atformatGlobalVirtualStorePath, the single point every global-virtual-store slot path funnels through — closing the same escape in the isolated linker, the resolver's dependency-graph builder, and the config-dependency installer.f8058eb: Reject symlinkedpnpm-lock.yamlfiles when reading or writing the env lockfile document.9318a11: AllowregistriesandnamedRegistriesto be configured in the globalconfig.yamlfile.51300fd: Fixed a path traversal vulnerability where a dependency whose manifestnamewas a scoped path traversal (e.g.@x/../../../<path>) could be written outsidenode_modulesto an attacker-controlled location duringpnpm install, even with--ignore-scripts. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.14332f0: Fail instead of silently removing an optional dependency's locked entries frompnpm-lock.yamlwhen the registry cannot resolve it. Previously, when registry metadata lacked a version that the lockfile already pinned (for example, a mirror that had not synced a recent release yet),pnpm installandpnpm dedupesilently dropped the optional dependency's entries — emptying maps such as the platform binaries of@napi-rs/canvas— so the lockfile differed between machines and frozen installs on other hosts had nothing to link #12853.fecfe83: Fixed peer dependency resolution withautoInstallPeerswhen a workspace package depends on a version of a package that a transitive dependency's self-contained closure also provides for itself. The peer providers that are attached to the root project for reuse are no longer peer-resolved a second time in the root context, so packages inside such a closure no longer get their peers bound to the root project's incompatible version #4993.5a4daec:${...}environment-variable placeholders in thehttpProxy,httpsProxy,noProxy,proxy, andnoproxysettings are no longer expanded when these settings come from a project'spnpm-workspace.yaml. They now receive the same protection already applied toregistry,namedRegistries, andpnprServer.d1da02e:pnpm publishno longer prints credentials when the target registry is configured with inlineuser:pass@credentials (e.g.registry=https://user:pass@example.com/). They are now redacted both from the "publishing to registry" line and from the OIDC (trusted publishing) failure messages.dcfc611:pnpm self-updatenow honorstrustPolicy=no-downgrade. It resolves the target pnpm version against full registry metadata, so it refuses to switch to a version whose supply-chain trust evidence is weaker than an earlier-published one, the same way a regular install does.a8ad82d: Register thepnalias in generated shell completion scripts.25bd5c3: Fixed standalone installer downgrades from pnpm v12 to v11.23996e9:pnpm runtime set <name> <version>now validates its arguments: the name must benode,deno, orbun, and the version must not contain a comma. Previously these were interpolated straight into apnpm addselector, where an unsupported name or a comma (e.g.node 22,is-positive) could be misread as a list of packages or a local directory and install unintended packages or bins.Platinum Sponsors
Gold Sponsors
v11.10.0: pnpm 11.10Compare Source
Minor Changes
e2e3c81: Added theissuescommand as an alias ofbugs, sopnpm issuesopens the package's bug tracker URL in the browser.8491f8e: Added theprefixcommand which prints the current package prefix directory (or global prefix directory if-g/--globalis used).3425e80: Added an_authsetting for configuring registry authentication as a single structured (URL-keyed) value. It can be set in the global pnpm config (config.yaml) or, for CI, via thepnpm_config__authenvironment variable. The env form sidesteps the GitHub Actions / bash / zsh limitation that broke the existingpnpm_config_//host/:_authToken=…form (env var names containing/,:, or.are silently dropped). Closes #12314.The value is keyed by registry URL so each secret is explicitly bound to the host that may receive it. Registry URL keys must use
httporhttpsand must not include credentials, query strings, or fragments:The equivalent in the global
config.yaml:Within each registry URL,
@means registry-wide/default credentials and package scopes like@orgbind credentials to that scope on the same host. The only supported credential field isauthToken(maps to_authToken/ bearer auth); the deprecatedbasicAuth/username+passwordforms are intentionally not accepted here.Each entry also infers a trusted registry route:
@routes the default registry (andpnpm add <pkg>resolves there), and@orgroutes that scope. Because the credential and destination host arrive in one trusted value, repo-controlledpnpm-workspace.yamlor project.npmrccannot redirect the token to a different host._authis honored only from the env var and the global config — it is ignored in a projectpnpm-workspace.yaml/.npmrc, so repo-controlled config can never supply registry auth. Precedence: CLI flags (--registry,--@scope:registry) >pnpm_config__auth> globalconfig.yaml_auth>pnpm-workspace.yaml.Both
pnpm_config__auth(lowercase, documented form) andPNPM_CONFIG__AUTH(all-caps, the shell convention some CI runners apply) are honored. If both are set, lowercase wins unless it is empty, in which case uppercase is used. The env var wins over the globalconfig.yaml_authon a conflicting key.tokenHelperis not supported in_auth. Parsing is strict: a malformed value (bad JSON, wrong shape, invalid registry URL or scope, an unsupported credential field) fails fast with an error rather than being silently dropped.Pacquet parity note: the pacquet (Rust) port supports the same single credential field as the TS CLI:
authToken.a33eeec:pnpm self-updateandpackageManagerversion-switching can now install and link pnpm v12 (the Rust port), published with equal content under both thepnpmand@pnpm/exenames on thenext-12dist-tag. Its native binaries ship as@pnpm/exe.<platform>-<arch>packages, which pnpm's built-in installer links directly — no Node.js launcher, so the command pays no Node startup cost. v12 is initialized exactly like@pnpm/exe, including per-platform global-virtual-store hashing. From v12 onward the install converges on the unscopedpnpmpackage (the Rust exe) — even when updating from the SEA@pnpm/exebuild.1dd12bd: When resolving through a pnpr install-accelerator server, pnpm no longer forwards its own upstream registry credentials in the resolve request. Only theAuthorizationheader identifying the caller to pnpr is sent. The pnpr server now selects upstream credentials from its own route policy (operator-configured upstream credential aliases), so private dependencies resolve through a pnpr-managed alias the caller is authorized to use, rather than by sending the client's registry tokens to the server.1e81761: Expose web authenticationauthUrlanddoneUrlin JSON error output when OTP is required in a non-interactive terminal #12724.Patch Changes
2f389d6: Added the Node.js release team's new signing key (Stewart X Addison,655F3B5C1FB3FA8D1A0CA6BDE4A7D232B936D2FD) to the embedded Node.js release keys, so runtimes whoseSHASUMS256.txtis signed by the new releaser verify successfully.acbdb94: Fixed shell tab completion not suggesting workspaces after the-Falias for--filteroption.dcabb78: Fixedpnpm up -r <pkg>bumping unrelated packages that have open semver ranges. Previously, any update mutation nullified the lockfile-derivedpreferredVersionsglobally, so packages with^x.y.zranges could re-resolve to newer compatible versions even though the user only asked to update a specific package. The install layer now always seedspreferredVersionsfrom the lockfile, and caller-supplied preferred versions (such as the vulnerability penalties ofpnpm audit --fix) layer on top of the seed instead of replacing it. The targeted package still bumps: the per-resolveupdateRequestedflag makes the resolver ignore the target's own lockfile pins.Closes #10662.
d539172: Fixed pnpm pack and pnpm publish failing when prepack generates files that are included in the package and postpack cleans them up.be6505a: Hardened global package management:node.exeflavor of a bin, so a stalenode.exeno longer survives onPATHafter uninstall, and a new global install no longer silently overwrites an existingnode.exe.pnpm add -g pnpm@<version>(and@pnpm/exe@<version>) is now rejected like the barepnpmform, pointing topnpm self-update.node_modulespaths, preventing a tampered manifest from escaping the install directory.25c7388: pnpm now rejectsjsr:specifiers whose package name is not a valid npm package name — an empty scope or name (e.g.jsr:@scope/), path separators inside the name, or any other shapevalidate-npm-package-namerejects — withERR_PNPM_INVALID_JSR_PACKAGE_NAMEinstead of silently converting them into a malformed@jsr/...npm package name.25c7388: pnpm now rejects named-registry specifiers (e.g.gh:) whose package name is not a valid npm package name — an empty scope (e.g.gh:@/bar), path separators inside the name (e.g.gh:@scope/../name), or any other shapevalidate-npm-package-namerejects — withERR_PNPM_INVALID_NAMED_REGISTRY_PACKAGE_NAMEinstead of passing the name through to registry URLs and metadata cache file paths.96da7c5: node-gyp'sgyp_main.pyandgypentrypoints are now packed with the executable bit in thepnpmand@pnpm/exetarballs. Without it, building native addons from source could fail with a permission error.99982b9: Sped up resolution and reduced memory use against registries that ignore npm's abbreviated metadata format and always return the full package document (for example, Azure DevOps Artifacts). pnpm now strips such documents down to the abbreviated field set before caching them. Resolution output is unchanged, and registries that honor the abbreviated format (such as the npm registry) pay no extra cost.11a7fdd: Sped up offline and--prefer-offlineresolution on large workspaces (e.g.pnpm dedupe --offline,pnpm install --offline). Package metadata loaded from the local cache is now kept in memory, so each package's metadata is parsed once per command instead of once per dependent that references it.2c7369d:pnpm pack-appnow rejects--entry/pnpm.app.entryand--output-dir/pnpm.app.outputDirvalues that are absolute paths or escape the project directory via..(or a symlink that resolves outside it), and refuses to write the produced executable when its target path already exists as a symlink (or other non-regular file). This prevents a repository-controlledpackage.jsonfrom embedding host files (such as an SSH key) into the produced executable, writing build artifacts outside the project, or overwriting an arbitrary file through a committed symlink. The new error codes areERR_PNPM_PACK_APP_ENTRY_OUTSIDE_PROJECT,ERR_PNPM_PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT, andERR_PNPM_PACK_APP_OUTPUT_FILE_NOT_REGULAR.When ad-hoc signing macOS targets,
pnpm pack-appnow runs the systemcodesignby absolute path and resolvesldidto a location outside the project, so a repository-controllednode_modules/.binonPATHcannot hijack the signer.ce5d5a5: Relative paths inpatchedDependenciesare now resolved against the lockfile directory when computing patch file hashes, so runningpnpm installfrom a subdirectory no longer fails withENOENTlooking for the patch file in the wrong location #12762.ebb4096:pnpm peersno longer reports a conflict for a missing peer dependency that is ignored viapnpm.peerDependencyRules.ignoreMissing.dcabb78: Fixed a prototype-pollution hazard when seeding preferred versions: a dependency named__proto__in a manifest or inpnpm-lock.yamlcould write throughObject.prototype(or crash the install) while the preferred-versions map was being built. The maps are now null-prototype objects, so crafted package names land as plain keys.f38e696: Hardenedpnpm deploy --forceso it refuses unsafe deploy targets such as workspace roots, parent directories, out-of-workspace paths, and symlinked target parents.806c3ec: pnpm no longer warns about ignored project-level auth settings whenPNPM_CONFIG_NPMRC_AUTH_FILEpoints at the project.npmrc— setting it to that file is an explicit opt-in to trusting it, so auth env variables in it are expanded pnpm/pnpm#12480.991405e: Restore differential rendering (ansi-diff) to fix duplicated output lines introduced by #12351.c121235: Fixed the topological order of--filtered commands (pnpm run,pnpm exec,pnpm publish,pnpm pack,pnpm rebuild) when the selected projects depend on each other only transitively through projects that were not selected. Previously such selected projects could run concurrently or in the wrong order; now a project always runs after the selected projects it transitively depends on, while projects without a real dependency relationship still run concurrently. This now also holds for prod-only filters (--filter-prod), which resolve order through the production dependency graph so transitive production dependencies are respected without pulling back the dev dependencies the filter drops, and for selections that mix--filterwith--filter-prod#8335.d539172:pnpm packandpnpm publishno longer follow a symlinked workspaceLICENSEfile when injecting it into a package that has no license of its own. Following the symlink could pack bytes from outside the workspace into the published tarball.dcabb78: Fixedpnpm up <pkg>producing a different result than a fresh install of the same manifests would. The resolver now distinguishesupdateRequested(true only for packages that match the user's update target) from the broaderupdateflag, and for the targeted package ignores only its own lockfile-derived preferred-version pins — so the target re-resolves exactly as if its lockfile entries were deleted andpnpm installran. Preferred versions a fresh install applies (manifest pins, versions propagated down the dependency chain, and the vulnerability-avoidance penalties ofpnpm audit --fix) stay in effect, so an update never installs duplicate versions that a reinstall from scratch would not reproduce. When a preferred version holds the update target below the newest version its range admits, pnpm now prints a warning explaining that reaching the newer version everywhere requires an override.dcabb78:pnpm update <dep>@<version>now prints a warning when<dep>is only present as a transitive dependency: the requested version cannot be applied there (updates resolve the target the way a fresh install would), and the warning recommends adding the version topnpm.overridesinstead, which is the mechanism that does pin transitive dependencies. Closes #12744.a6c4d5f: When a dependency cannot be found in the registry (404) or the registry has no matching version, and a workspace project with the same name exists only at non-matching versions, the error now reports the available workspace versions (ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE) instead of the raw registry failure pnpm/pnpm#1379. Other registry failures (authorization, network, server errors) still propagate unchanged. The pacquet (Rust) resolver applies the same behavior.Platinum Sponsors
Gold Sponsors
v11.9.0: pnpm 11.9Compare Source
Minor Changes
bae694f: Some registries generate tarballs on-demand and cannot provide an integrity checksum in their package metadata. In that case pnpm now computes the integrity from the downloaded tarball and stores it in the lockfile, so the entry is verifiable on subsequent installs instead of being written without an integrity (which would fail the next install). This also applies to--lockfile-only: the tarball is downloaded so its integrity can be computed. A lockfile entry that is still missing its integrity is rejected as aERR_PNPM_MISSING_TARBALL_INTEGRITYlockfile verification violation (the install fails closed) rather than being silently re-fetched.6c35a43: Added--exclude-peerstopnpm sbom. Withauto-install-peers(the default), peer dependencies resolve into the lockfile and are otherwise indistinguishable from the package's own dependencies. The flag drops peer dependencies (and any transitive subtree reachable only through them) from the SBOM. CycloneDX 1.7 has no scope or relationship that expresses "consumer-provided peer", so omission is the only spec-clean handling. The flag name matchespnpm list --exclude-peers; note the SBOM flag prunes a peer's exclusive subtree, which is stricter thanpnpm list(which only hides leaf peers).Patch Changes
25a829e:pnpm audit --fixnow writes a single combinedminimumReleaseAgeExcludeentry per package (e.g.axios@0.18.1 || 0.21.1) instead of one entry per version, matching the format documented for the setting. Existing per-version entries inpnpm-workspace.yamlare merged into the combined form rather than left as duplicates. Installs that auto-collect immature versions intominimumReleaseAgeExcludenow report the same combined entries, so the "Added N entries" message matches what is written to the manifest #12534.1cbb5f2: Fixed non-deterministic peer resolution that could add or remove an optional transitive peer — for example@babel/core, reached throughstyled-jsx— from a package's peer-dependency suffix across otherwise identical installs, churning the lockfile and causing intermittentpnpm dedupe --checkfailures in CI. When a package's children are resolved by one occurrence (the "owner") and reused by a deeper consumer, whether that consumer inherited the owner's missing peers depended on whether the owner's resolution had finished yet — a race under concurrent resolution. The decision is now a function of the dependency graph's structure rather than resolution-completion order.d577eea: Fixed a Windows flakiness inpnpm dlxwhere a failed install could surface a spuriousEBUSY: resource busy or lockederror. The cleanup of a partially-populated dlx cache is now best-effort with retries and no longer masks the original error.ec7cf70: Shortened thepnpm dlxcache path so deep dependency trees no longer overflow Windows'MAX_PATH, which could make a dependency's lifecycle script fail withspawn cmd.exe ENOENT.05b95ab: Fixedpnpmhanging (and crashing with an unhandled promise rejection) when a non-retryable network error such asSELF_SIGNED_CERT_IN_CHAINoccurs while fetching from a registry. The error is now rejected through the returned promise instead of being thrown inside the detached retry callback.d3f68e2: Fix apnpm auditperformance regression on lockfiles that contain dependency cycles. The reachable-vulnerability pruning added in pnpm 11.5.1 only memoized acyclic subtrees, so any node whose subtree touched a cycle — together with all of its ancestors — was recomputed on every query, making the path walk quadratic. Reachability is now computed once per node using Tarjan's strongly-connected-components algorithm, so cyclic graphs are handled in linear time #12212.The audit path walk also no longer recurses, so a deeply nested dependency graph can no longer overflow the call stack, and the install path to each finding is tracked without per-node copying, keeping memory linear in the graph depth.
322f88f: Fix failed optional dependency updates so they don't rewrite unrelated dependency specs #11267.1488db1: WhenenableGlobalVirtualStoreis toggled on for a project that was previously installed without it, stale hoisted symlinks undernode_modules/.pnpm/node_modulesare now replaced instead of being left pointing at the old per-project virtual store location #9739.6545793: Fixedpnpm install --ignore-workspaceoverwriting theallowBuildsmap inpnpm-workspace.yaml. The ignored builds of a package with a build script were auto-populated intoallowBuildseven though--ignore-workspacewas passed, clobbering committedtrue/falsevalues with theset this to true or falseplaceholder #12469.fbdc0eb: FixedminimumReleaseAgeExcludeandtrustPolicyExcludeso multiple exact-version entries for the same package behave the same as a single||disjunction entry. Previously only the first matching rule's versions were honored, so a config like[form-data@4.0.6, form-data@2.5.6]could still flagform-data@2.5.6as violatingminimumReleaseAge, while[form-data@4.0.6 || 2.5.6]worked as expected #12463.fa7004b: The in-memory package metadata cache is now populated on the exact-version disk fast path, so repeated resolutions of the same package within one install no longer re-read and re-parse the on-disk metadata. In large monorepos this brings the time for adding a new package down from minutes to seconds. The in-memory cache key now also includes the registry, so a package of the same name served by two different registries in a single install can no longer share a cache slot and resolve the wrong tarball.0a154b1: Fixedpnpm patchdropping the package name (and leaking internal option fields) when the patched dependency resolves to a single git-hosted version.4d3fe4b: The pnpr resolver endpoints moved under the reserved/-/pnprnamespace:POST /v1/resolveis nowPOST /-/pnpr/v0/resolveandPOST /v1/verify-lockfileis nowPOST /-/pnpr/v0/verify-lockfile. The capability handshake atGET /-/pnpradvertises protocol version0to match. This keeps every pnpr-proprietary route in npm's reserved namespace, so it can never collide with a package path.0ec878d: Removing a runtime dependency now removes the matchingdevEngines.runtimeorengines.runtimeentry that was materialized from it. Blank runtime selectors are normalized tolatest.17e7f2c:pnpm sbomnow emits a CycloneDXissue-trackerexternal reference for components (and the root) whosepackage.jsondeclares abugsURL. Email-onlybugsentries are skipped, since the reference requires a URL.a84d2a1: Add@pnpm/resolving.tarball-url, which builds and recognizes the canonical npm tarball URL of a package. It vendorsgetNpmTarballUrl(previously the externalget-npm-tarball-urlpackage) and addsisCanonicalRegistryTarballUrl, the predicate the lockfile writer uses to decide whether a tarball URL is derivable from name+version+registry (and can therefore be omitted frompnpm-lock.yaml).Exposing
isCanonicalRegistryTarballUrllets a custom resolver (pnpmfileresolvers) fronting a proxy that serves tarballs on a non-canonical path (e.g. an ephemerallocalhost:<port>) rewrite the resolved tarball to the canonical form, so nothing host-specific is persisted to the lockfile. Previously this logic was private to@pnpm/lockfile.utils.Two correctness fixes are included while consolidating the logic: the scoped-package unescape now handles uppercase
%2Fas well as%2f(percent-encoding is case-insensitive), and protocol-insensitive comparison strips only a leadinghttp(s)://scheme instead of splitting on the first://(which could truncate URLs containing a later://).852d537: Lockfile verification no longer reports a registry metadata fetch failure (for example a403/401on a private registry, or a network error) asERR_PNPM_TARBALL_URL_MISMATCH. When the registry can't be reached to verify an entry, the install now aborts with the registry's own fetch error (such asERR_PNPM_FETCH_403, which already explains the authentication situation) instead of mislabeling a transport failure as lockfile tampering. Registry fetch errors no longer leak basic-auth credentials embedded in the registry URL (https://user:pass@host/) into their message.Platinum Sponsors
Gold Sponsors
v11.8.0: pnpm 11.8Compare Source
Minor Changes
c112b61: Added a--dry-runoption topnpm install. It runs a full dependency resolution and reports what an install would change, but writes nothing to disk (no lockfile, nonode_modules) and always exits with code 0. This mirrors the preview semantics ofnpm install --dry-run#7340.179ebc4:pnpm run --no-bailnow exits with a non-zero exit code when any of the executed scripts fail, while still running every matched script to completion. This makes the exit-code behavior of--no-bailconsistent between recursive and non-recursive runs (recursive runs already failed at the end). Previously, a non-recursivepnpm run --no-bailalways exited with code 0, even when a script failed #8013.0474a9c: Added support for generating Node.js package maps atnode_modules/.package-map.jsonduring isolated and hoisted installs. Added thenode-experimental-package-mapsetting to inject the generated map into pnpm-managed Node.js script environments, and thenode-package-map-typesetting to choose betweenstandardandloosepackage maps.dcededc:pnpm sbomnow marks components reachable only throughdevDependencieswith CycloneDXscope: "excluded"and thecdx:npm:package:developmentproperty. Theexcludedscope documents "component usage for test and other non-runtime purposes", which matches the semantics of a devDependency; the property is the CycloneDX npm-taxonomy marker emitted by@cyclonedx/cyclonedx-npm, so both modern (scope) and existing (property) consumers are covered. Components reachable at runtime (including installedoptionalDependencies) omitscopeand default torequired.1495cb0: Added per-package SBOM generation with--outand--splitflags. Use--out out/%s.cdx.jsonto write one SBOM per workspace package to individual files, or--splitfor NDJSON output to stdout. When--filterselects a single package, the SBOM root component now uses that package's metadata. Workspace inter-dependencies (workspace:protocol) and their transitive dependencies are included. Author, repository, and license fall back to the root manifest when the package doesn't define them.293921a: feat(view): support searching project manifest upward when package name is omittedWhen running
pnpm viewwithout a package name, the command now searchesupward for the nearest project manifest (
package.json,package.yaml, orpackage.json5) and uses itsnamefield.If the manifest exists but lacks a
namefield, an error is thrown.This change also replaces the
find-updependency withempathicforimproved performance and consistency across workspace tools.
Patch Changes
29ab905: Fixedpnpm updateoverriding the version range policy of a named catalog whose name parses as a version (e.g.catalog:express4-21). Thecatalog:reference carries no pinning of its own, so the prefix from the catalog entry (such as~) is now preserved instead of being widened to^#10321.bee4bf4: Security: validate config dependency names and versions from the env lockfile (pnpm-lock.yaml) before using them to build filesystem paths. A committed lockfile with a traversal-shapedconfigDependenciesname (such as../../PWNED) or version (such as../../../PWNED) could previously causepnpm installto create symlinks or write package files outsidenode_modules/.pnpm-configand the store. Names must now be valid npm package names and versions must be exact semver versions; the same validation is applied to optional subdependencies of config dependencies, and to the legacy workspace-manifest format before any lockfile is written. See GHSA-qrv3-253h-g69c.96bdd57: Fixlink:workspace protocol switching tofile:afterpnpm rmis run from inside a workspace package whose target workspace dependency has its own dependencies, wheninjectWorkspacePackages: trueis set. Follow-up to #10575, which fixed the same symptom for workspace packages without dependencies.302a2f7: No longer warn about using bothpackageManageranddevEngines.packageManagerwhen the two fields pin the same package manager at the same version with the same integrity hash (e.g. bothpnpm@11.5.1+sha512.…). Previously the hash was stripped from the legacypackageManagerfield but not fromdevEngines.packageManager, so even identical specifications looked like a mismatch #12028.The warning still fires on any genuine divergence, and several cases now state the specific reason instead of a single generic message: a different package manager, a different version, or contradictory integrity hashes for the same version.
3f0fb21: Fixed the progress line showing leftover characters from external processes that write to the terminal between progress updates (e.g. an SSH passphrase prompt would leave a fragment likeadded 0sa':). The interactive reporter now redraws each frame in place, erasing to the end of the display before reprinting, so any such remnants are cleared #12350.564619f: Fixedpnpm approve-buildsreporting "no packages awaiting approval" when a build-script dependency whose approval was revoked (e.g. aftergit stashdrops theallowBuildsfrompnpm-workspace.yaml) is re-added. The revoked packages are now correctly recorded in.modules.yamlsoapprove-buildscan find them. #122213d1fd20: Skip the redundant "target bin directory already contains an exe called node" warning on Windows when the existingnode.exealready matches the target (same hard link or identical content) pnpm/pnpm#12203.1b02b47: Fix macOS Gatekeeper blocking native binaries (.node,.dylib,.so) by removing thecom.apple.quarantineextended attribute after importing them from the store.When pnpm imports files from its content-addressable store into
node_modules, macOS preserves extended attributes, includingcom.apple.quarantine. If this xattr is present on a store blob (e.g. it was first written under a Gatekeeper-enabled app such as a Git client), it propagates tonode_modules, and Gatekeeper blocks the native binary from loading even though pnpm already verified the file's integrity against the lockfile.After importing a package, pnpm now strips
com.apple.quarantinefrom its native binaries, matching Homebrew's behaviour of dropping quarantine from verified downloads. The cleanup is macOS-only, runs in a single batchedxattrcall per package, is restricted to native binaries (other files are untouched), and is non-fatal (it logs a warning on unexpected errors).Fixes #11056
61969fb: Fixpnpm installwithoptimisticRepeatInstallincorrectly reportingAlready up to datewhenpnpm-lock.yamlchanged but project manifests did not. This affected workflows such as checking out or restoring only the lockfile #12100.Also fixes
checkDepsStatusto use the correct lockfile path whenuseGitBranchLockfileis enabled, so the optimistic fast-path and lockfile modification detection work withpnpm-lock.<branch>.yamlfiles instead of always stat'ingpnpm-lock.yaml. Merge-conflict detection now reads the resolved lockfile name as well, and withmergeGitBranchLockfilesenabled everypnpm-lock.*.yamlis scanned for modifications and conflicts. The git branch is now resolved by reading.git/HEADdirectly (no process spawn) and uses the workspace directory rather thanprocess.cwd().5c12968: Fix recursive updates of transitive dependencies when the update command mixes transitive dependency patterns with direct dependency selectors. For example,pnpm up -r "@babel/core" uuidnow updates matching transitive@babel/coredependencies even whenuuidis a direct dependency selector #12103.9d79ba1: Register thepnpm update --no-saveflag in the CLI help and option parser.0474a9c: Fixedpnpm importfor Yarn v2 lockfiles whenjs-yamlv4 is installed.9e0c375: Fixedpnpm installrepeatedly prompting to remove and reinstallnode_modulesin a workspace package whenenableGlobalVirtualStoreis enabled. The post-install build step recorded a per-projectnode_modules/.pnpmvirtual store directory innode_modules/.modules.yaml, overwriting the global<storeDir>/linksvalue the install step had written. The next install then detected a virtual-store mismatch (ERR_PNPM_UNEXPECTED_VIRTUAL_STORE). The build step now derives the same global virtual store directory as the install step #12307.223d060: Document the--cpu,--osand--libcflags in the output ofpnpm install --help. These flags were already supported but were only documented on the website #12359.e85aea2: Avoid readingREADME.mdfrom disk when publishing if the publish manifest already provides areadmefield. The README is now only read lazily, insidecreateExportableManifest, when it is actually needed.3188ae7: Fixedpnpm peers checkto accept loose peer dependency ranges such as>=3.16.0 || >=4.0.0-when the installed peer version satisfies the range #12149.531f2a3: Fixedpnpm updaterewriting aworkspace:dependency that points at a local path (e.g.workspace:../packages/foo/dist) into a normalizedlink:or version-range specifier. Such specifiers are now preserved verbatim when the workspace protocol is preserved #3902.fe66535: Fixed a lockfile non-convergence bug where an incremental install kept a duplicate transitive dependency that a fresh install would not produce. When a package is reused from the lockfile, its child edges are taken verbatim and bypass the preferred-versions walk, so a transitive dependency could stay pinned to an older version even after a direct dependency resolved to a higher version that satisfies the same range. The resolver now refreshes such a stale pin to the higher direct-dependency version during resolution — so the older version is never resolved or fetched, and the incremental result converges to the fresh one.6d35338:pnpm installdetects changes inside local file dependencies again. The optimistic repeat-install fast path only tracks manifest and lockfile modification times, so edits inside a local dependency's directory (or a repacked local tarball) were reported as "Already up to date". Projects with local file dependencies (file:and bare local path or tarball specifiers, declared directly or throughpnpm.overrides) now always run a full install, which refetches those dependencies, matching pnpm v10 behavior #11795.4ca9247: Preserve the existing Node.js runtime version prefix when resolvingnode@runtime:<range>to a concrete version.30c7590: Create shorter CAFS temporary package directories to leave room for lifecycle scripts that create IPC socket paths under TMPDIR.13815ad: Reporter output (warnings, progress) forpnpm storeandpnpm configsubcommands now goes to stderr instead of stdout. This fixes scripts that capture their stdout (e.g.PNPM_STORE=$(pnpm store path),pnpm config list --json | jq) from getting warnings mixed into the result.1c05876: Avoid relinking unchanged child dependencies and remove stale child links during warm installs.817f99d: Fixed lockfile churn where a package'stransitivePeerDependenciescould be dropped (and shift between packages) when the package participates in a dependency cycle. A cycle re-entry resolves against truncated children, so it must not be cached as "pure"; otherwise sibling occurrences of the same package short-circuit and lose transitive peers depending on traversal order #5108.eba03e0: Fixpnpm installreporting "Already up to date" after a catalog entry inpnpm-workspace.yamlwas reverted to a previous version. After an update modified a catalog, the workspace state cache stored the pre-update catalog versions, so reverting the entry back to its original version was not detected as an outdated state #12418.3b54d79:pnpm updatenow keeps lockfileoverridesthat resolve through a catalog in sync with the catalog. Previously, when an override referenced a catalog (e.g.overrides: { foo: 'catalog:' }) andpnpm updatebumped that catalog entry, the lockfile'scatalogsadvanced while the resolvedoverrideskept the old version. The resulting lockfile was internally inconsistent, so a laterpnpm install --frozen-lockfilefailed withERR_PNPM_LOCKFILE_CONFIG_MISMATCH.9d0a300: Fixedpnpm version --recursiveso it honors the workspace selection. In recursive mode the version bump now applies to the packages resolved from the workspace filter (selectedProjectsGraph), matching the behavior ofpnpm publish --recursive, instead of always bumping every workspace package #11348.Platinum Sponsors
Gold Sponsors
v11.7.0: pnpm 11.7Compare Source
Minor Changes
Added a new setting
frozenStore(--frozen-store) that letspnpm installrun against a package store on a read-only filesystem (e.g. a Nix store, a read-only bind mount, an OCI layer). When enabled, pnpm opens the store's SQLiteindex.dbthrough theimmutable=1URI — bypassing the WAL/-shmsidecar creation that otherwise fails on a read-only directory — and suppresses every store-write path (theindex.dbwriter and the project-registry write). Pair it with--offline --frozen-lockfileagainst a fully-populated store. Under the global virtual store, package directories live inside the store, so if the store is missing the build output of a package whose lifecycle scripts are approved (or that has a patch), pnpm fails up front withERR_PNPM_FROZEN_STORE_NEEDS_BUILDrather than crashing mid-build on a read-only write — seed the store with those builds first. Incompatible with--forceand with a configured pnpr server, since both write into the store; the side-effects cache is likewise not written underfrozenStore. If the store is missing its content directory, the install fails fast withERR_PNPM_FROZEN_STORE_INCOMPLETErather than attempting to initialize it. The read-onlyimmutable=1open requires Node.js >=22.15.0, >=23.11.0, or >=24.0.0; on older runtimes--frozen-storefails with a clearERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODEerror. Bin-linking also tolerates a read-only store: under the global virtual store a package's bin source lives inside the store, so thechmodthat makes it executable would be refused — withEPERM/EACCES, or withEROFSon a genuinely read-only filesystem. Thatchmodis redundant when the seed already ships its bins executable with a normalized shebang, so it is now skipped in that case, while a non-executable bin (or one still carrying a Windows CRLF shebang) on a read-only store still errors.When
pacquet(the Rust port of pnpm) is declared inconfigDependencies, pnpm now delegates dependency resolution to it too — not just materialization — provided the installed pacquet is new enough to support full resolving installs (>= 0.11.7).Previously pacquet only ran in frozen-install mode: pnpm always resolved the dependency graph itself (writing
pnpm-lock.yaml) and handed pacquet a finished lockfile to fetch / import / link. With pacquet >= 0.11.7, a non-frozenpnpm install(default isolatednodeLinker, plain install) is delegated to pacquet end-to-end in a single pass — pacquet resolves the manifests, writes the lockfile, and materializesnode_modules. pnpm detects the capability from the installed pacquet's version; older pacquet releases keep the resolve-then-materialize split, andadd/update/removestill resolve in pnpm (it has to mutate the manifests first). This remains an opt-in preview of the Rust install engine #11723.Added a new opt-in
--batchflag topnpm publish --recursivethat sends all selected packages to the registry in a singlePUT /-/pnpm/v1/publishrequest instead of one request per package. The target registry has to implement the batch publish endpoint (pnpr does); registries that don't are reported with a clearERR_PNPM_BATCH_PUBLISH_UNSUPPORTEDerror. The batch is processed all-or-nothing by pnpr: if any package in the batch fails validation, none of the packages are published.Patch Changes
Reject path-traversal and reserved dependency aliases (such as
../../../escape,.bin,.pnpm, ornode_modules) that come from a lockfile rather than a freshly resolved manifest. A crafted lockfile alias could otherwise be joined directly under a hoistednode_modulesdirectory, letting package files be written outside the intended install root or overwrite pnpm-owned layout.The fix adds two layers:
nodeLinker: hoistedgraph builder now validates each alias at the directory sink (safeJoinModulesDir), matching the validation pnpm already performs when resolving aliases from manifests.verifyLockfileResolutions) now runs an always-on, policy-independent check that rejects any importer or snapshot dependency alias that is not a valid package name, failing the install early — before any fetch or filesystem work — for every node linker at once.Made shared package child resolution deterministic when the same package is reached through multiple contexts. pnpm now chooses the shallowest occurrence, then importer order, then parent path, instead of letting request timing decide the child context and missing-peer report pnpm/pnpm#12358.
Fix garbled summary line after submitting
pnpm update -iandpnpm audit --fix -i. The interactive checkbox prompt previously printed every selected choice's full table row (label, current/target versions, workspace, URL) joined by commas, producing a wall of text after pressing Enter. The summary now lists only the selected package names (or vulnerability keys) by setting an explicitshortper choice; the in-progress selection UI is unchanged.Prevent
pnpm patch-removefrom removing files outside the configured patches directory.Fixed
pnpm publishignoringstrictSsl: falsewhen publishing to registries with self-signed certificates. ThestrictSSLoption is now forwarded tolibnpmpublish/npm-registry-fetchso thatstrict-ssl=falsein.npmrcorstrictSsl: falseinpnpm-workspace.yamlis respected during publish, the same way it is forpnpm installpnpm/pnpm#12012.Fixed
Cannot destructure property 'manifest' of 'manifestsByPath[rootDir]' as it is undefinedregression introduced in 11.6.0 when runningpnpm add <pkg>outside a workspace on Windows.selectProjectByDirwas keying the resultingProjectsGraphbyopts.dirinstead ofproject.rootDir, so downstreammanifestsByPathlookups missed when the two paths normalized differently (typically drive-letter casing). pnpm/pnpm#12379Git dependencies that point to a subdirectory of a repository (
repo#commit&path:/sub/dir) keep theirpathin the lockfile again. Since the integrity of git-hosted tarballs started being pinned in the lockfile, any install that actually downloaded the tarball rebuilt the lockfile resolution as{ integrity, tarball, gitHosted }and dropped thepathfield, while installs served from the store kept it — so the field disappeared seemingly at random. Withoutpath, later installs from that lockfile silently unpacked the repository root instead of the subdirectory #12304.Fixed nondeterministic lockfile output that made
pnpm dedupe --checkfail intermittently in CI. When a locked peer provider was pinned for a dependency that has no child dependencies of its own, the pinned provider leaked into the shared parent scope, so siblings resolved after it could pick up an optional peer they should not see. Which siblings were affected depended on resolution order, which varies with network timing.Sped up
pnpm installwith a frozen lockfile by running lockfile verification (the policy revalidation gate added forminimumReleaseAge/trustPolicyand the tarball-URL anti-tamper check) concurrently with fetching and linking instead of blocking the whole install on it. Dependency lifecycle scripts are still held back until verification succeeds, so no script runs on an unverified lockfile: if verification fails the install aborts before any dependency build, and if linking finishes first the install waits for the verification verdict before completing.User-defined
npm_config_*environment variables are now preserved during lifecycle script execution. Previously, allnpm_-prefixed env vars were stripped, which caused user-set variables likenpm_config_platform_archto be lost pnpm/pnpm#12399.pnpm can now use different auth tokens for different package scopes, even when those scopes use the same registry URL.
Previously, auth was selected only by registry URL. If
@org-aand@org-bboth usedhttps://npm.pkg.github.com/, they had to share the same token. This caused problems for registries that issue tokens per organization or per scope.Configure a scope-specific token by adding the package scope after the registry URL in the auth key:
pnpm login --registry=https://npm.pkg.github.com --scope=@org-awrites the token to the same scope-specific auth key.When installing or publishing
@org-a/*, pnpm usesORG_A_TOKEN. For@org-b/*, pnpm usesORG_B_TOKEN. Packages without a matching scope continue to use the registry-wide fallback token.pnpm setupno longer prompts to approve build scripts for@pnpm/exewhen installing the standalone executable. pnpm links the platform-specific binary itself, so the package's install scripts are skipped during the global self-install #12377.Close lockfile reads deterministically before rewriting lockfiles and keep pacquet's virtual store directory length aligned with pnpm on Windows.
A
304 Not Modifiedanswer from the registry now renews the cached metadata file's mtime, so theminimumReleaseAgefreshness shortcut keeps serving resolutions from the cache. Previously, once a cached packument grew older thanminimumReleaseAge, every subsequent install re-validated it against the registry forever, because a 304 never rewrites the file.Updated dependency ranges. Notably:
@pnpm/loggerpeer dependency range moved to^1100.0.0.msgpackr1.11.8 → 2.0.4 (store index files remain byte-compatible in both directions).open^7.4.2 → ^11.0.0,memoize^10 → ^11,cli-truncate^5 → ^6,pidtree^0.6 → ^1.@yarnpkg/core4.5.0 → 4.8.0,@rushstack/worker-pool0.7.7 → 0.7.18,@cyclonedx/cyclonedx-library10.0.0 → 10.1.0,@pnpm/config.nerf-dart^1 → ^2,@pnpm/log.group3.0.2 → 4.0.1,@pnpm/util.lex-comparator^3 → ^4.Updated
@zkochan/cmd-shimto v9.0.6.Fixed a Windows-only hang where a failed command could take 20–46 seconds to exit. On error, pnpm enumerates descendant processes (via
pidtree) to terminate them, which on Windows shells out towmic/PowerShellGet-CimInstance Win32_Process— a lookup that is extremely slow on some machines. The lookup is now bounded by a short timeout so it can no longer stall the process exit.Platinum Sponsors
Gold Sponsors
v11.6.0: pnpm 11.6Compare Source
⚠️ Security fix — environment variables in a project
.npmrc(action may be required)Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands
${ENV_VAR}placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:.npmrc—registry,@scope:registry, proxy URLs, URL-scoped keys (//host/…), and credential values (_authToken,_auth,_password,username,tokenHelper,cert,key);pnpm-workspace.yaml.Environment variables are still expanded in trusted config: your user-level
~/.npmrc, the global config, CLI options, and environment config.If your authentication broke after upgrading, move the token out of the committed
.npmrc:Or keep the
${NPM_TOKEN}line but put it in your user-level~/.npmrcinstead of the repo. In GitHub Actions,actions/setup-nodewithregistry-urlalready writes a user-level.npmrc, soNODE_AUTH_TOKENkeeps working. For other CI where editing each pipeline is hard, setPNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc(orNPM_CONFIG_USERCONFIG=.npmrc) in the CI environment to declare the project.npmrctrusted.See https://pnpm.io/npmrc for full migration details.
Minor Changes
pnpm installcompletes without re-resolving whenpnpm-lock.yamlwas deleted butnode_modulesis intact: the up-to-date check now treats the current lockfile (node_modules/.pnpm/lock.yaml) — the record of what the previous install materialized — as the wanted lockfile, verifies the manifests still match it, restorespnpm-lock.yamlfrom it, and reports "Already up to date". Previously this scenario triggered a full resolution and a re-verification of every locked package against the registry.615c669: Added support for configuring URL-scoped registry settings throughnpm_config_//…andpnpm_config_//…environment variables, for example:This provides a file-free way to supply registry authentication. Because the registry a value applies to is encoded in the (trusted) environment variable name, it is host-scoped by construction and cannot be redirected to another registry by repository-controlled config. The environment value is treated as trusted config: it takes precedence over a project/workspace
.npmrcbut is still overridden by command-line options. When the same key is provided through both prefixes,pnpm_config_wins.Raised the default network concurrency from
min(64, max(cpuCores * 3, 16))tomin(96, max(cpuCores * 3, 64)). Package downloads are I/O-bound, not CPU-bound, so deriving the floor from the core count left machines with few cores (for example 4-vCPU CI runners) downloading only 16 tarballs at a time and unable to saturate a low-latency registry. ThenetworkConcurrencysetting still overrides the default.Patch Changes
.npmrcuses an environment variable in a registry/proxy URL or in registry credentials. The message now explains why the setting was ignored and how to migrate it to a trusted source — for example by moving the line to the user-level~/.npmrcor runningpnpm config set "<key>" <value>— with a link to https://pnpm.io/npmrc. Thepnpm config setexample is only suggested when the key has no${...}placeholder, so the snippet is always safe to copy-paste.os/cpu/libcfields are missing from the registry metadata or the lockfile. Some registries strip these fields from the package metadata, which made pnpm download and install the binaries of every platform regardless ofsupportedArchitectures. The missing platform fields of an optional dependency are now inferred from its name (e.g.@nx/nx-win32-arm64-msvc→os: win32,cpu: arm64), so foreign-platform binaries are skipped without even downloading them #11702.Platinum Sponsors
Gold Sponsors
v11.5.3: pnpm 11.5.3Compare Source
⚠️ Security fix — environment variables in a project
.npmrc(action may be required)Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands
${ENV_VAR}placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:.npmrc—registry,@scope:registry, proxy URLs, URL-scoped keys (//host/…), and credential values (_authToken,_auth,_password,username,tokenHelper,cert,key);pnpm-workspace.yaml.Environment variables are still expanded in trusted config: your user-level
~/.npmrc, the global config, CLI options, and environment config.If your authentication broke after upgrading, move the token out of the committed
.npmrc:Or keep the
${NPM_TOKEN}line but put it in your user-level~/.npmrcinstead of the repo. In GitHub Actions,actions/setup-nodewithregistry-urlalready writes a user-level.npmrc, soNODE_AUTH_TOKENkeeps working. For other CI where editing each pipeline is hard, setPNPM_CONFIG_NPMRC_AUTH_FILE=.npmrc(orNPM_CONFIG_USERCONFIG=.npmrc) in the CI environment to declare the project.npmrctrusted.See https://pnpm.io/npmrc for full migration details.
Patch Changes
Stopped expanding environment variables in repository-controlled registry/proxy request destinations and registry credential values from
.npmrc, and in workspace registry URLs frompnpm-workspace.yaml. Move dynamic registry URL and token configuration to trusted user, global, CLI, or environment config.Resolve package-manager bootstrap dependencies with trusted user or CLI registry and network config, and reject package-manager env-lockfile records that do not use registry package paths with integrity-only resolutions before auto-switch execution.
Avoid writing
packageManagerDependenciestopnpm-lock.yamlwhen package manager policy is set toonFail: ignoreorpmOnFail: ignore#12228.Avoid running dependency-status auto-install when the dependency status is unavailable without a project manifest.
Using the
$version reference syntax inoverrides(e.g."react": "$react") now prints a deprecation warning. The syntax still works, but catalogs are the recommended way to keep an overridden version in sync with the rest of the workspace. Reference a catalog entry with thecatalog:protocol instead.Fixed
pnpm config get globalconfigto return the globalconfig.yamlpath again pnpm/pnpm#11962.Fixed bare
--colorso it does not consume the following CLI flag, allowing command shorthands like--parallelto expand correctly and forms likepnpm --color with current <command>to dispatch the inner command instead of failing withMISSING_WITH_CURRENT_CMD.Fix
pnpm installignoringenableGlobalVirtualStoretoggle by including it in the workspace state settings check #12142.Security: pnpm now verifies the npm registry signature of a package-manager binary before spawning it, so a cloned repository cannot make pnpm download and execute an arbitrary native binary.
This covers two paths that select an executable from repository-controlled input:
pacquet(or@pnpm/pacquet) inconfigDependenciesopts in to pnpm's Rust install engine. pnpm now verifies that the installedpacquetshim and the host's@pacquet/<platform>-<arch>binary carry a valid npm registry signature for their exactname@version, and refuses to run pacquet (failing the command) if the signature does not verify or cannot be checked. The only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.self-update— thepackageManager/devEngines.packageManagerfield makes pnpm download and run a specific pnpm version. pnpm now verifies the registry signature ofpnpm,@pnpm/exe, and the host platform binary before installing/spawning them, and refuses to run an engine whose signature does not match a published, signed release. The check runs only on an actual download (store cache miss), so it does not add a network round trip to every command.In both cases the signature is verified over the installed integrity, against npm's public signing keys that ship embedded in the pnpm CLI (like corepack), so bytes substituted via a tampered lockfile or a repository-controlled registry fail verification — and a registry the user did not vouch for cannot supply its own signing keys. The signed packument is fetched from the configured registry, so an npm mirror works transparently. Verification fails closed: if it cannot be completed (for example, the registry is unreachable), the command fails rather than running an unverified binary. The embedded keys are kept current by a release-time check against npm's signing-keys endpoint.
Made peer-dependent deduplication deterministic. When a peer-suffixed package variant was a subset of two or more mutually incompatible larger variants, the variant it collapsed into depended on the order importers were resolved in, which varies between machines. This could resolve the same workspace to different lockfiles on different platforms and make
pnpm dedupe --checkalternate between passing and failing.Reject invalid package names and versions from staged tarball manifests before deriving filenames for
pnpm stage download.Clarified in CLI help that the pnpm store is trusted shared state and store integrity checks are corruption detection, not a tamper boundary for untrusted store writers.
Reject reserved manifest
binnames ("",".","..", and scoped forms such as@scope/..) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted.Require trusted package identity before package-name
allowBuildsentries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as theallowBuildskey. Lockfile verification now rejects lockfiles where a registry-style dependency path (name@semver) is backed by a git, directory, or git-hosted tarball resolution (ERR_PNPM_RESOLUTION_SHAPE_MISMATCH), so the dependency path is a reliable artifact identity by the time scripts can run.Security: pnpm now verifies the OpenPGP signature of a downloaded Node.js runtime's
SHASUMS256.txtbefore trusting its integrity hashes.When a repository requests a Node.js runtime (e.g. via
devEngines.runtime/useNodeVersion), the download mirror is repository-configurable throughnode-mirror:<channel>. The integrity of the downloaded binary was only checked againstSHASUMS256.txtfetched from that same mirror — a circular check that a malicious mirror could satisfy by serving a tampered binary together with a matchingSHASUMS256.txt. pnpm then executes the binary (for example to run lifecycle scripts).pnpm now fetches
SHASUMS256.txt.sigand verifies the detached OpenPGP signature against the Node.js release team's public keys, which ship embedded in the pnpm CLI. A mirror that serves a tampered binary cannot also produce a valid signature, so the download fails to verify. The embedded keys are kept current by a release-time check against the canonicalnodejs/release-keyslist.The musl variants from the hardcoded
unofficial-builds.nodejs.orgmirror are not repository-configurable and are signed by a different key, so they continue to be trusted over TLS.Platinum Sponsors
Gold Sponsors
v11.5.2: pnpm 11.5.2Compare Source
Patch Changes
Peer dependency resolution now reuses the peer contexts already recorded in the lockfile when those providers are still present in the dependency graph and still satisfy the peer ranges. This avoids unnecessary peer-context rewrites during lockfile regeneration. Current manifest choices remain authoritative: a newly added, explicitly updated, or aliased direct provider, a changed nested provider, or a locked version that no longer satisfies the range still takes precedence.
The lockfile verifier now checks that a registry entry pinning an explicit
tarballURL points at the artifact the registry's own metadata lists for thatname@version. Previously a tampered lockfile could pair a trustedname@versionwith an attacker-chosen tarball URL (and a matching integrity for those bytes), so the install fetched the attacker's bytes. A mismatch — or any entry that can't be confirmed against the registry — is rejected withERR_PNPM_TARBALL_URL_MISMATCH. Non-registry resolutions (file:, git-hosted, etc.) and registry entries without an explicit tarball URL (the URL is reconstructed from name+version+registry, so it is inherently bound) are unaffected; non-standard registry tarball URLs (npm Enterprise, GitHub Packages) still pass because they match the metadata.Fix
pnpm update --recursive --lockfile-only <pkg>@<version>crashing withInvalid Versionwhen the catalog entry for<pkg>is a version range (e.g.^21.2.10) andcatalogModeisstrictorprefer. The catalog–version comparison now skips the equality check when either side is a range rather than passing a range tosemver.eq(), so range specifiers fall through to the existing mismatch handling instead of throwing #11570.Avoided a Node.js crash when pnpm exits after network requests on Windows.
Fixed packages being materialized into the virtual store without their root-level files (
package.json,LICENSE, README, root entrypoints) when multiplepnpm installprocesses ran against the same store/workspace concurrently. The fast import path used to destructively empty the shared target directory, so a concurrent importer could wipe files another importer had already written; if the surviving files included thepackage.jsoncompletion marker, every later install treated the broken directory as complete and never repaired it. The fast path now imports directly only when it can create the target directory exclusively, and otherwise builds the package in a private temp directory and atomically renames it into place #12197.Fix dependency build scripts not running under the global virtual store (
enableGlobalVirtualStore).In a workspace install, dependency build scripts are deferred to a single
rebuildpass (buildProjects). That pass resolved each package's location from the classicnode_modules/.pnpm/<depPathToFilename>layout, which does not exist under the global virtual store — so native dependencies (e.g. packages usingnode-gyp/prebuild-install) were never built and failed to load at runtime (Cannot find module .../build/Release/*.node).buildProjectsnow resolves the global-virtual-store projection directory (<storeDir>/links/<hash>, computed with the same graph hash the installer uses) whenenableGlobalVirtualStoreis set, and serializes concurrent builds of the same shared projection so parallel workspace projects don't race on the same directory.Don't promote a
runtime:dependency (such as the Node.js version fromdevEngines.runtimeorpnpm runtime set) into a catalog whencatalogModeisstrictorprefer. Aruntime:dependency round-trips todevEngines.runtime, which only recognizes theruntime:protocol; cataloging it rewrote the manifest entry tocatalog:, which broke that round-trip, stranded it indevDependencies, and leftdevEngines.runtimeuntouched.Skip lockfile
minimumReleaseAge/trustPolicyverification for non-registry tarball protocols (for examplefile:), so local tarball dependencies are not incorrectly checked against npm registry metadata.Platinum Sponsors
Gold Sponsors
v11.5.1: pnpm 11.5.1Compare Source
Patch Changes
pnpm auditperformance by pruning non-vulnerable lockfile subtrees and stopping path enumeration once vulnerable findings reach the path cap.npm_config_user_agentfor root lifecycle scripts during headless installs.integrityfield of a remote (non-registry) tarball dependency when its lockfile entry is rebuilt. Re-resolving such a dependency without re-fetching it (for example viapnpm update, or when another dependency changes) produced a resolution with no integrity — URL/tarball resolvers only learn the integrity after the tarball is downloaded — so the previously recorded integrity was dropped, making later installs fail withERR_PNPM_MISSING_TARBALL_INTEGRITY#12067.repositoryfield into the{ type, url }object form when creating the publish manifest, matching npm's behavior. Some registries (e.g. Gitea/Codeberg) reject a stringrepositorywith a 500 Internal Server Error duringpnpm publish#12099.@typescript-eslint/eslint-pluginpeer-depends on both@typescript-eslint/parserandtypescript, and@typescript-eslint/parserpeer-depends ontypescript), pnpm no longer reuses a hoisted instance of the shared peer that was resolved against a different version #12079.Platinum Sponsors
Gold Sponsors
v11.5.0: pnpm 11.5Compare Source
Minor Changes
Added a new
hoistingLimitssetting fornodeLinker: hoistedinstalls, mirroring yarn'snmHoistingLimits. It acceptsnone(the default — hoist as far as possible),workspaces(hoist only as far as each workspace package), ordependencies(hoist only up to each workspace package's direct dependencies). Originally proposed in #6468, closing #6457.Replaced
enquirerwith@inquirer/promptsfor all interactive prompts. Fixes theupdate -iscrolling overflow bug where long choice lists were clipped in the terminal #6643.User-facing changes:
pnpm update -i/pnpm update -i --latest: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination viausePaginationpnpm audit --fix -i: Same scrolling fix for vulnerability selectionpnpm approve-builds: Interactive build approval prompts updatedpnpm patch: Version selection and "apply to all" prompts updatedpnpm patch-remove: Patch removal selection updatedpnpm publish: Branch confirmation prompt updatedpnpm login: Credential prompts updatedpnpm run/pnpm exec(withverifyDepsBeforeRun=prompt): Confirmation prompt updatedVim-style
j/kkeys still work for up/down navigation in all interactive prompts.Internal: The
OtpEnquirerandLoginEnquirerDI interfaces changed from{ prompt }to{ input }/{ input, password }respectively. Plugins or custom builds that inject their own enquirer mock will need to update.Staged publishes are now recognized in the trust scale. When a package version's registry metadata carries an
approverfield, it is treated as the strongest trust evidence (ranked above trusted publishers and provenance attestations), since staged publishes require 2FA publish approvals. This prevents false-positive trust downgrade errors when moving from a staged publish to a lower trust level #11887.Patch Changes
Fix pnpm hanging during peer resolution when an aliased install pulls in transitive packages with mutual peer cycles at different depths in the dependency tree (for example,
pnpm i nuxt@npm:nuxt-nightly@5x). Cycles whose members hit thefindHitcache instead of running their owncalculateDepPathare now short-circuited by sibling resolutions at the level where the cycle is detected, so the cached path promises no longer deadlock. #11999.Fix
pnpm dist-tag addandpnpm dist-tag rmagainst npmjs.org failing without--otpwith[ERR_PNPM_UNAUTHORIZED] You must be logged in to set dist-tag … "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA.". pnpm now sendsnpm-auth-type: webon dist-tag writes and surfaces the resulting OTP challenge through the existing browser-based 2FA flow (the samewithOtpHandlinghelper used bypnpm publish), so the browser opens, the user authenticates, and the dist-tag is set on retry.--otp=<code>continues to work via the classic flow.Fix
minimumReleaseAgeExcludehandling in npm resolution fast paths so excluded packages do not get pinned to stale versions. Excludes are honored consistently duringpublishedBymetadata selection and cache-mtime shortcuts.Fix the
integrityfield being dropped from the lockfile entry of a remote (non-registry) https-tarball dependency when an unrelated package is installed afterwards. URL/tarball resolvers do not return an integrity (it is only known after the tarball is downloaded), so when such a dependency was reused from the lockfile without being re-fetched, its integrity was lost. It is now carried over from the existing resolution. With pnpm's lockfile-integrity hardening, the missing integrity made subsequent--frozen-lockfileinstalls fail withERR_PNPM_MISSING_TARBALL_INTEGRITY. #12001.Skip dependency re-resolution when
pnpm-lock.yamlis missing butnode_modules/.pnpm/lock.yamlexists and still satisfies the manifest.pnpm installnow reuses the materialized snapshot to regeneratepnpm-lock.yamlinstead of walking the registry to rebuild it from scratch, turning the cache+node_modules variation into a near-no-op for users who deleted the lockfile but kept the install #11993.--frozen-lockfilestill refuses to proceed whenpnpm-lock.yamlis absent — the regenerated lockfile must be committed, so failing loudly is the correct behavior for CI.Platinum Sponsors
Gold Sponsors
v11.4.0: pnpm 11.4Compare Source
Minor Changes
Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously,
pnpm install(non-frozen) would logERR_PNPM_TARBALL_INTEGRITY, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.pnpm installnow exits withERR_PNPM_TARBALL_INTEGRITYand a hint pointing at the new opt-in flag.The only opt-in is
pnpm install --update-checksums— narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.--forceandpnpm updatedeliberately do not bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide.--frozen-lockfilebehavior is unchanged.--fix-lockfilekeeps its documented purpose (filling in missing lockfile entries) and is also not a bypass.pnpm runtime set <name> <version>now saves the runtime todevEngines.runtimeby default instead ofengines.runtime. Pass--save-prod(or-P) to save it toengines.runtimeinstead #11948.Patch Changes
Fix a credential disclosure issue where an unscoped
_authToken(or_auth, orusername+_password, ortokenHelper) defined in one source —~/.npmrc,~/.config/pnpm/auth.ini, a workspace.npmrc, CLI flags, etc. — would be sent as anAuthorizationheader to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (cert,key) so they aren't presented to a registry their author didn't choose.pnpm now rewrites each unscoped per-registry setting (
_authToken,_auth,username,_password,tokenHelper,cert,key) to its URL-scoped form at load time, using theregistry=value declared in the same source (or the npmjs default registry if the source declares none). A later layer overridingregistry=therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended.ca/cafileare intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally.Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since
npm@9, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g.//registry.example.com/:_authToken=...or//registry.example.com/:cert=...).@pnpm/network.auth-header: removed thedefaultRegistryparameter fromcreateGetAuthHeaderByURIandgetAuthHeadersFromCreds. Now that credentials are URL-scoped at load time, the mergedconfigByUrinever contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed.Fix
pnpm deploycrashing withENOENT: ... lstat '<deployDir>/node_modules'whenconfigDependenciesdeclares pacquet (pacquetor@pnpm/pacquet). The deploy directory never installs config dependencies, so the install engine they designate isn't on disk to invoke; the nested install now skips them.Reject git resolutions whose
commitfield is not a 40-character hexadecimal SHA before invokinggit. A malicious lockfile could otherwise smuggle a value such as--upload-pack=<command>throughgit fetch/git checkout, which on SSH or local-file transports executes the supplied command.Limit concurrent project manifest reads while listing large workspaces to avoid
EMFILEerrors.Reject patch files whose
diff --githeaders reference paths outside the patched package directory. Previously a malicious.patchfile added via a pull request could write, delete, or rename arbitrary files reachable by the user runningpnpm install.Improve the log message that pnpm prints after auto-adding entries to
minimumReleaseAgeExcludewhenminimumReleaseAgeis set withoutminimumReleaseAgeStrict. The message previously referred to the internal "loose mode" terminology, which wasn't searchable in the docs; it now tells the user to setminimumReleaseAgeStricttotrueif they want these updates gated behind a prompt instead #11747.Reject dependency aliases that contain path-traversal segments (such as
@x/../../../../../.git/hooks) when reading them from a package manifest or symlinking them intonode_modules. A malicious registry package could otherwise use a transitive dependency key to makepnpm installcreate symlinks at attacker-chosen paths outside the intendednode_modulesdirectory.Reject
pnpm-lock.yamlentries whose remote tarballresolution:block is missing theintegrityfield. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that stripsintegrity:) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under--frozen-lockfile. pnpm now fails closed at lockfile-read time withERR_PNPM_MISSING_TARBALL_INTEGRITY. Git-hosted tarballs (gitHosted: trueor a URL on codeload.github.com / bitbucket.org / gitlab.com) andfile:tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.Validate
devEngines.runtimeandengines.runtimeversion ranges fornode,deno, andbunwhenonFailis set toerrororwarn. Previously these settings only had an effect withonFail: 'download'— theerrorandwarnmodes silently did nothing #11818. Violations now throwERR_PNPM_BAD_RUNTIME_VERSION.Require provenance before treating trusted publisher metadata as the strongest trust evidence.
Platinum Sponsors
Gold Sponsors
v11.3.0: pnpm 11.3Compare Source
Minor Changes
Added
pnpm stagewithpublish,list,view,approve,reject, anddownloadsubcommands for npm staged publishing.Added a new setting
trustLockfile. Whentrue,pnpm installskips the supply-chain verification pass that re-appliesminimumReleaseAge/trustPolicy='no-downgrade'to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author. Defaults tofalse; verification stays on by default. Set inpnpm-workspace.yaml.Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (
~4klockfile entries withminimumReleaseAge+trustPolicy: no-downgradeenabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (time, per-version_npmUser.trustedPublisher,dist.attestations.provenance). The abbreviated-metadata cache is similarly projected to just the package-levelmodifiedfield and the set of currently-listed version names. Fixes #11860.Implemented
pnpm pkgcommand natively, followingnpm pkgstandards.Implemented
pnpm repocommand natively, followingnpm repostandards.Implemented
pnpm set-script(aliasss) natively. Adds or updates an entry in thescriptsfield of the project manifest, supportingpackage.json,package.json5, andpackage.yamlformats.Add a
skip-manifest-obfuscationoption forpnpm packandpnpm publish. When enabled, the originalpackageManagerfield and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specificpnpmfield continues to be omitted.Patch Changes
pnpm dlxfailing withERR_PNPM_NO_IMPORTER_MANIFEST_FOUNDwhen the installed package's CAS slot is missing itspackage.json. Observed in the wild forpnpm dlx node@runtime:<version>when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship apackage.jsonof their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, sodlxnow falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including everyruntime:spec) this matches whatmanifest.binwould have named. Multi-bin packages already require--package=<spec> <bin>to disambiguate and don't enter this code path.pnpm dedupeandpnpm installwhen a dependency graph contains packages with transitive peer dependencies on each other (e.g.@aws-sdk/client-stsand@aws-sdk/client-sso-oidc) andauto-install-peersis enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was thatresolveDependenciespushed onto itspkgAddresses/postponedResolutionsQueuearrays from insidePromise.all-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes #8155.pnpm add <github-shorthand>(and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs orpnpm/test-git-fetch#sha) was silently dropped from the manifest update and frompendingBuilds. The alias-keyed lookup added in that PR couldn't find awantedDependencywhosealiaswasundefinedat parse time but resolved to a package name only after fetching, so the entry never made it intospecsToUpsert. Restored the original index-based pairing betweendirectDependenciesandwantedDependencies; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven byrdd.catalogLookup.userSpecifiedBareSpecifier, not by the lookup. Fixes the threerebuilds dependencies/rebuilds specific dependencies/rebuild with pending optionfailures inbuilding/commands/test/build/index.ts.pnpm add --configleaving orphan entries inpnpm-lock.env.yaml(the optional subdependencies of the previously resolved version of the updated config dependency).Platinum Sponsors
Gold Sponsors
v11.2.2: pnpm 11.2.2Compare Source
Minor Changes
Experimental: Adding
@pnpm/pacquet(the Rust port of pnpm) toconfigDependenciesinpnpm-workspace.yamlnow delegates the materialization phase ofpnpm installto the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.To configure pacquet in a project, run:
You'll see changes in
pnpm-workspace.yamlandpnpm-lock.yamlthat should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.Patch Changes
configDependencies, the user's CLI flags passed topnpm install(e.g.--no-runtime,--prod,--dev,--no-optional,--node-linker,--cpu/--os/--libc,--offline,--prefer-offline) are now forwarded to pacquet'sinstallsubcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like--no-runtimewere silently dropped. Flag forwarding is gated on the command beinginstall/i;add,update, anddedupestill don't forward (their flag surface doesn't line up with pacquet'sinstall).pnpm up(andpnpm add/pnpm remove) failing withpacquet_package_manager::outdated_lockfilewhen pacquet is declared inconfigDependencies. pnpm now passes--ignore-manifest-checkto pacquet so its--frozen-lockfilecheck doesn't fire against the (pre-mutation)package.jsonpnpm hasn't written yet #11797. Requires a pacquet release that supports the flag — bumpPACQUET_VERSIONin the e2e tests once it ships.Platinum Sponsors
Gold Sponsors
v11.2.1: pnpm 11.2.1Compare Source
Minor Changes
Experimental: Adding
@pnpm/pacquet(the Rust port of pnpm) toconfigDependenciesinpnpm-workspace.yamlnow delegates the materialization phase ofpnpm installto the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.To configure pacquet in a project, run:
You'll see changes in
pnpm-workspace.yamlandpnpm-lock.yamlthat should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.Patch Changes
optional: truein the env lockfile, matching how optional dependencies are recorded elsewhere inpnpm-lock.yaml. Previously, snapshots for the platform-specific subdeps pulled in via a config dep'soptionalDependencieswere written as empty objects, which was inconsistent with the rest of the lockfile and made it look like those non-host platform variants were required.pickRegistryForPackagereturning the wrong registry for an unscopednpm:alias under a scoped local name. A manifest entry like"@private/foo": "npm:lodash@^1"was routing thelodashfetch throughregistries["@private"], even thoughlodashis unscoped and doesn't live on that registry. The npm-alias branch now returns the alias target's own scope (ornullfor an unscoped target, falling through toregistries.default) instead of leaking into the local key's scope.Platinum Sponsors
Gold Sponsors
v11.2.0: pnpm 11.2Compare Source
Minor Changes
Experimental: Adding
@pnpm/pacquet(the Rust port of pnpm) toconfigDependenciesinpnpm-workspace.yamlnow delegates the materialization phase ofpnpm installto the pacquet binary. pnpm still owns dependency resolution; pacquet only fetches and imports from the freshly-written lockfile. This is an opt-in preview of the Rust install engine #11723.To configure pacquet in a project, run:
You'll see changes in
pnpm-workspace.yamlandpnpm-lock.yamlthat should be committed. If you experience any issues with pacquet, please let us know by mentioning this in the GitHub issue you create.configDependenciesnow resolve and install one level ofoptionalDependenciesdeclared by the config dependency, withos/cpu/libcplatform filtering applied at install time. This unlocks the esbuild/swc-style pattern where a package ships platform-specific binaries viaoptionalDependencies— a config dependency can now do the same and have the matching binary symlinked next to it in the global virtual store, sorequire('pkg-platform-arch')from inside the config dependency resolves correctly.The env lockfile records all platform variants regardless of host platform, so it remains portable across machines. Each entry in a config dependency's
optionalDependenciesmust declare an exact version — ranges and tags are rejected to keep installs reproducible.Implement the documented
pnpm login --scope <scope>flag. The scope is normalized (a leading@is added if missing; blank values are ignored) and an@<scope>:registry=<registry>mapping is written to the pnpm auth file alongside the auth token. Subsequent installs of@<scope>/*packages then route to the chosen registry. Previouslypnpm login --scope fooerrored withUnknown option: 'scope'despite the flag being listed in the online documentation #11716.pnpm outdatedandpnpm update --interactivenow report Node.js, Deno, and Bun runtimes installed as project dependencies (runtime:specifiers). Previously these were silently skipped.Patch Changes
Fix
cafile=<relative-path>in.npmrcbeing read from the wrong directory when pnpm is invoked from a different cwd (e.g.pnpm --dir <project> installfrom a CI wrapper or monorepo script). The path is now resolved against the directory of the.npmrcthat declared it, notprocess.cwd(). Before this fix the CA file silently failed to load — the install proceeded without the configured CA and the user only saw TLS errors against a private registry, with no log line tying back to the wrongly resolved path #11624.Fix
config.registrygetting a trailing slash appended whenregistryis set in.npmrcand noregistries.defaultis provided bypnpm-workspace.yaml. The sync fromregistries.defaulttoconfig.registryintroduced in #11744 now only fires when the workspace manifest actually contributes a different default.Fix global add/update to handle minimumReleaseAge policy violations instead of surfacing an internal resolver guardrail error.
Fix two crashes with
injectWorkspacePackages: truewhen the lockfile has been pruned (e.g. byturbo prune --docker):Cannot use 'in' operator to search for 'directory' in undefined: a peer-dependency-variant injected snapshot inherits itsresolutionfrom the basepackages:entry; when a pruner drops that base entry the readers crash.convertToLockfileObjectnow reconstructs the directory resolution from thefile:depPath at load time — a single normalization point, so every reader sees a fully-formed snapshot.ERR_PNPM_ENOENTonnode_modules/.bin/<tool>: afterprepare/postinstall,runLifecycleHooksConcurrentlyre-imported each injected workspace package; thescanDir-into-filesMapworkaround fed target-internal paths to the importer, which themakeEmptyDirfast path (#11088) then wiped. Drop the workaround and passkeepModulesDir: trueso the importer preserves the target's existingnode_modules(bin links + transitive deps) and source files keep their hardlinks.Fixed
pnpm loginandpnpm logoutignoringregistries.defaultfrompnpm-workspace.yaml#10099.Fix the
minimumReleaseAge(publishedBy) maturity shortcut to be inclusive at the cutoff. Previously, abbreviated metadata whosemodifiedfield equalled the cutoff fell off the fast path and triggered a full-metadata re-fetch (or aMISSING_TIMEerror when full metadata wasn't permitted). Sincemodifiedis an upper bound on every version's publish time,modified == publishedByalready implies every version passes the per-version<=filter infilterPkgMetadataByPublishDate, so the shortcut now accepts the boundary case directly. Strictly>(was>=) at the rejection branch.Honor
publishConfig.accesswhen publishing packages.Platinum Sponsors
Gold Sponsors
v11.1.3: pnpm 11.1.3Compare Source
Patch Changes
pnpm installnow re-validatespnpm-lock.yamlentries against the activeminimumReleaseAgeandtrustPolicy: 'no-downgrade'policies before any tarball is fetched. Lockfiles resolved elsewhere (committed to the repo, restored from a CI cache, produced by an older pnpm) under a weaker or absent policy can no longer install a freshly-published or trust-downgraded version silently. Violating entries abort the install withERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION,ERR_PNPM_TRUST_DOWNGRADE, or the genericERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATIONwhen both policies trip in the same batch;minimumReleaseAgeExcludeandtrustPolicyExcludeare honored. Verification results are cached so repeat installs against an unchanged lockfile take a fast path, and pnpm shows a transient progress line while the registry round-trip runs.When fresh resolution picks an immature version, the behavior depends on
minimumReleaseAgeStrict:minimumReleaseAgekeeps its built-in 24-hour value — auto-adds the immature picks tominimumReleaseAgeExcludeinpnpm-workspace.yamland lets the install proceed. A single info message lists what was persisted.minimumReleaseAgeExcludeand the install continues; declining aborts before the lockfile,package.json, ornode_modulesis touched.ERR_PNPM_NO_MATURE_MATCHING_VERSIONlisting every offending entry, instead of failing on the first one the resolver hit.minimumReleaseAgeStrictauto-enables whenever the user explicitly setsminimumReleaseAge(CLI flag, env var, globalconfig.yaml, orpnpm-workspace.yaml); setminimumReleaseAgeStrict: falseto keep loose-mode auto-collect even with an explicitminimumReleaseAgevalue. Closes #10438, #10488, #11687.Allow redundant trailing base64 padding in
.npmrcauth values and report invalid auth base64 with a pnpm error.Make
pnpm self-updaterespectminimumReleaseAge(andminimumReleaseAgeExclude) when resolving which pnpm version to install.When the
latestdist-tag points to a version newer than the configured age threshold,self-updatenow selects the newest mature version instead unless excluded byminimumReleaseAgeExclude.Also makes
dlxandoutdatedsurface invalidminimumReleaseAgeExcludepatterns under the sameERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDEerror code already used byinstall, instead of leaking the internalERR_PNPM_INVALID_VERSION_UNION/ERR_PNPM_NAME_PATTERN_IN_VERSION_UNIONcodes.Global installs respect global config build policy (e.g.,
dangerouslyAllowAllBuildsfrom config.yaml) when GVS is enabled #9249.The global virtual-store (GVS) default
allowBuilds = {}was applied before workspace manifest settings were read and before global config values (stripped byextractAndRemoveDependencyBuildOptions) were re-applied viaglobalDepsBuildConfig. This causedhasDependencyBuildOptionsto returntrue(because{}is not null), blocking restoration of global config values likedangerouslyAllowAllBuilds. As a result, global installs skipped all build scripts even when the config explicitly allowed them.This fix moves the GVS default to after workspace manifest reading and
globalDepsBuildConfigre-application, so that:allowBuildstakes precedence (if present)dangerouslyAllowAllBuildsis properly restored (if set and no workspace policy exists){}is only applied as a last resort when no policy is configured anywhereHonor
--silentwhenverifyDepsBeforeRun: installauto-installs dependencies beforepnpm runorpnpm exec, preventing install output from being written to stdout #11636.Fix lockfile parsing failures when
pnpm-lock.yamlcontains CRLF line endings and multiple YAML documents #11612.Anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node —
engines.runtimepin when present, shellnodeotherwise — instead of pnpm's own runtime.ENGINE_NAME(the<platform>;<arch>;node<major>prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed fromprocess.version— the Node that runs pnpm itself. That was wrong in two situations:@pnpm/exeSEA bundle. The bundle has its own embedded Node, not thenodeon the user'sPATHthat actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shellnode.engines.runtime/devEngines.runtimepin. When a project pins a Node version viadevEngines.runtime(pnpm v11+), pnpm downloads that Node intonode_modules/node/and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node.Three changes:
@pnpm/engine.runtime.system-node-versionnow exportsengineName(nodeVersion?). Resolves the version in this order: explicit override →getSystemNodeVersion()(which already prefersnode --versionoverprocess.versionin SEA contexts) →process.version.@pnpm/deps.graph-hashernow exportsfindRuntimeNodeVersion(snapshotKeys)— scans an iterable of lockfile snapshot keys for anode@runtime:<version>entry and returns its bare version string.calcDepStateandcalcGraphNodeHash/iterateHashedGraphNodesaccept anodeVersion?(in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded toengineName(). The default (no override) preserves the pre-change behaviour. The legacyENGINE_NAMEconstant in@pnpm/constantsis unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up.@pnpm/installing.deps-resolver,@pnpm/installing.deps-restorer,@pnpm/installing.deps-installer,@pnpm/building.during-install,@pnpm/building.after-install,@pnpm/deps.graph-builder) now derives the project's pinned runtime viafindRuntimeNodeVersion(Object.keys(graph))once per invocation and threads it through.On upgrade, two one-time GVS slot churns are possible:
node26) now hash under the shell-Node major (e.g.node24), matching what pacquet, the npm-publishedpnpmpackage, and any other pnpm-compatible tool already produce.devEngines.runtimepin: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on.In both cases the old slots become prune-eligible.
Resolve the GVS hash's engine portion per-snapshot when a dependency declares its own
engines.runtime, instead of using an install-wide value.Pnpm's resolver desugars a dep's
engines.runtimeintodependencies.node: 'runtime:<version>', and the bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into<pkgDir>/node_modules/node/. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so a pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose<platform>;<arch>;node<major>triple disagreed with the Node the build actually ran on.Per-snapshot resolution now matches what
bins/linkeralready does on a per-package basis:@pnpm/deps.graph-hasheraddsreadSnapshotRuntimePin(children)— reads thenodeentry from one snapshot's graph children and extracts the version from anode@runtime:value. Pairs with the existingfindRuntimeNodeVersion(snapshotKeys)install-wide fallback (also now exported from@pnpm/deps.graph-hasherrather than@pnpm/engine.runtime.system-node-version, where it was a poor fit —system-node-versionis about probing the host Node, not parsing lockfile-derived strings).calcDepStateandcalcGraphNodeHashconsultreadSnapshotRuntimePin(graph[depPath].children)first and only fall back to the install-widenodeVersionparameter when the snapshot doesn't pin its own Node.Pacquet mirrors the same precedence at the
calc_graph_node_hashcall site inpackage-manager/src/virtual_store_layout.rs— a newfind_own_runtime_node_major(snapshot)helper reads each snapshot'sdependenciesfor anodeentry withPrefix::Runtimeand overrides the install-wide engine when present.On upgrade, snapshots of dependencies that declare their own
engines.runtimere-hash under that dep's pinned Node instead of the install-wide value. The old slots become prune-eligible. Closes #11690.Fixed
pnpm publishfailing with a 404 when authentication relied on OIDC trusted publishing alongside an.npmrcwritten byactions/setup-node(_authToken=${NODE_AUTH_TOKEN}) withoutNODE_AUTH_TOKENbeing set. Unresolved${VAR}placeholders in auth values are now treated as empty rather than passed through verbatim, so the literal placeholder no longer surfaces as a bearer token when OIDC fallback is the intended auth source #11513.Fix
devEngines.packageManager(singular form, withoutonFail) defaulting toonFail: "error"instead of the documentedpmOnFail: "download". As a result, a project that pinned a different pnpm version viadevEngines.packageManagerand ranpnpm installfrom a mismatched pnpm version failed with a hard error, even though the migration table frommanagePackageManagerVersions: truetopmOnFail: download (default)promises the install would auto-download the wanted version #11676.The array form of
devEngines.packageManagerkeeps its existing per-element defaults (errorfor the last entry,ignorefor the rest), since those reflect explicit prioritization by the user. ExplicitonFailvalues continue to win.Fix
devEngines.packageManagernot writingpackageManagerDependenciestopnpm-lock.yamlwhen the lockfile lacks an env-doc entry. Previously the lockfile sync skipped resolution unless an existingpackageManagerDependencies.pnpmentry needed refreshing, so a fresh install withoutonFail: "download"left the resolved pnpm version unrecorded — contradicting the documented behavior that the resolved version is stored inpnpm-lock.yaml#11674.Warn when
package.jsoncontains a legacypnpmfield with settings pnpm no longer reads frompackage.json(e.g.pnpm.overrides,pnpm.patchedDependencies). Previously these were silently ignored after the upgrade from v10, leaving users unaware that their overrides/patched dependencies had stopped taking effect #11677.Platinum Sponsors
Gold Sponsors
v11.1.2: pnpm 11.1.2Compare Source
Patch Changes
Strip
sec-fetch-*headers from outgoing HTTP requests. These headers are automatically added by undici'sfetch()implementation per the Fetch spec but cause Azure DevOps Artifacts to return HTTP 400 for uncached upstream packages, as ADO interprets them as browser requests #11572.Fix
minimumReleaseAgehandling for cached abbreviated metadata.The version-spec cache fast path no longer rethrows
ERR_PNPM_MISSING_TIMEunderstrictPublishedByCheck; it now falls through to the registry-fetch path, consistent with the adjacent mtime-gated cache block.When the registry returns 304 Not Modified for a package whose cached metadata is abbreviated (no per-version
time), pnpm now re-fetches withfullMetadata: trueifminimumReleaseAgeis active and the package was modified after the cutoff. The upgraded metadata is persisted to disk so subsequent installs don't repeat the fetch. Previously the abbreviated meta was used as-is and the maturity check fell back to its warn-and-skip path, silently bypassing the quarantine and emitting a misleading "metadata is missing the time field" warning.Closes #11619.
Fix
pnpm upgrade --interactive --latest -rnot respecting named catalog groups. Previously, upgrading a dependency using a named catalog (e.g."catalog:foo") would incorrectly rewritepackage.jsonto"catalog:"and place the updated version in the default catalog instead of the named one #10115.Fixed
optimisticRepeatInstallskippingpnpm-lock.yamlmerge conflict resolution when the existingnode_modulesstate appears up to date.Fix
minimumReleaseAge/resolutionMode: time-basedinstalls failing on lockfiles whosetime:block is missing entries. The npm-resolver's peek-from-store fast path now surfacespublishedAtfrom the lockfile rather than discarding it, and falls through to a registry metadata fetch when the time-based cutoff can't be computed from the data on hand.Platinum Sponsors
Gold Sponsors
v11.1.1: pnpm 11.1.1Compare Source
Patch Changes
checkDepsStatus(run byverifyDepsBeforeRun). Previously the status check calledfindWorkspaceProjects, which validates each project'senginesandos/cpu/libcand warns about useless fields in non-root manifests — work that the install pipeline already performs. With nonodeVersionthreaded through, the engine check also fell back to the system Node fromPATHand emitted spurious "Unsupported engine" warnings before scripts ran. Status-only callers now usefindWorkspaceProjectsNoCheck; install paths continue to validate.pnpm add <alias>:@scope/pkgfor named registries. The local resolver was claiming any specifier containing/as a local directory, sopnpm add bit:@teambit/bit(withbitconfigured undernamedRegistries) installed a bogus link tobit:@teambit/bit/instead of resolving from the configured registry. The local resolver now runs after the named-registry resolver in the resolution chain.@zkochan/cmd-shimto 9.0.3. The sh shim it writes for.cmd/.battargets now escapes the/Cswitch as//C, so it survives the path translation Git Bash applies when launchingcmd.exe. Without this, a bare/Cwas rewritten toC:\before reaching cmd.exe — the switch was dropped, cmd started interactively, and the calling script saw the cmd banner instead of the wrapped command's output. Affects any cmd-shim-wrapped batch script invoked from Git Bash / MSYS / Cygwin on Windows. See pnpm/cmd-shim#55.Platinum Sponsors
Gold Sponsors
v11.1.0: pnpm 11.1Compare Source
Minor Changes
Added
pnpm audit signaturesto verify ECDSA registry signatures for installed packages against keys from/-/npm/v1/keys#7909. Scoped registries are respected, and registries without signing keys are skipped.Added support for installing packages from the GitHub Packages npm registry via a built-in
gh:prefix (e.g.pnpm add gh:@acme/private), and, more broadly, for arbitrary named registries in the style of vlt's named-registry aliases. Authentication is picked up from the existing per-URL.npmrcentries (e.g.//npm.pkg.github.com/:_authToken=...), so no separate auth mechanism is required.Additional aliases — or an override for the built-in
ghalias, for GitHub Enterprise Server — can be configured undernamedRegistriesinpnpm-workspace.yaml:With this,
work:@corp/lib@^2.0.0resolves againsthttps://npm.work.example.com/. #11324.Allow setting sbom spec version using
--sbom-spec-version#11389.Add
--no-runtimeflag (config:runtime=false) to skip installing runtime entries (e.g. Node.js downloaded viadevEngines.runtime) without modifying the lockfile. The lockfile keeps the runtime entry so frozen-lockfile validation still passes; only the runtime fetch and.binlinking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. viapnpm runtime -g set node <version>) beforepnpm installruns.Added the
pnpm bugscommand that opens a package's bug tracker URL in the browser. With no arguments, it reads the current project'spackage.json; with one or more package names, it fetches each package's metadata from the registry and opens its bug tracker. Falls back to<repository>/issueswhen thebugsfield is missing #11279.Added
pnpm ownercommand to manage package owners on the registry.Patch Changes
Added "published X ago by Y" information to the
pnpm viewcommand output, similar tonpm view. This is useful when comparing againstminimumReleaseAge.For example,
pnpm view pnpmnow shows:pnpm publishnow honors the configured HTTP/HTTPS proxy (includinghttps_proxy/http_proxy/no_proxyenvironment variables) when polling the registry'sdoneUrlduring the web-based authentication flow. Previously the poll bypassed the proxy, causing the registry to respond403from a different source IP and the login to never complete #11561.pnpm add -gnow installs each space-separated package into its own isolated directory by default. To bundle multiple packages into the same isolated install (so that they share dependencies and are removed together), pass them as a comma-separated list. For example:pnpm add -g foo barinstallsfooandbaras two independent globals — removing one does not affect the other.pnpm add -g foo,bar qarbundlesfooandbarinto a single isolated install whileqaris installed on its own.Related: #11587.
pnpm runtime set <name> <version>no longer fails in the root of a multi-package workspace with theADDING_TO_ROOTerror. Installing the workspace root is a valid target for a runtime, so the command now bypasses that safety check.Fix
pnpm --versionhanging for the lifetime of the worker pool after the version was printed.main.ts's--versionshort-circuit returned before reaching the command-handlerfinallythat callsfinishWorkers(), so the worker pool thatswitchCliVersionhad spawned during integrity resolution stayed alive and held the Node event loop open. The CLI entry now runsfinishWorkers()from its ownfinally, so every exit path tears the pool down.Repro:
pnpm --versionin a workspace whosedevEngines.packageManagerversion already matches the running pnpm +onFail: "download".switchCliVersionresolves the integrity (spawning workers), finds nothing to swap, returns. The version prints, then the process hangs.Platinum Sponsors
Gold Sponsors
v11.0.9: pnpm 11.0.9Compare Source
Patch Changes
https://gitlab.com/<user>/<project>/-/archive/<sha>/<project>-<sha>.tar.gzinstead of the GitLab API endpoint that contained an encoded slash (%2F) between user and project. The encoded slash both triggered406 Not Acceptableresponses from GitLab and produced virtual store directory names that Node refused to import (ERR_INVALID_MODULE_SPECIFIER) #11533.NPM_CONFIG_USERCONFIG(and its lowercasenpm_config_userconfigform) as a low-priority fallback when locating the user-level.npmrc. This restores compatibility with environments that point npm at a custom auth file via that env var — most notablyactions/setup-node, which writes registry credentials to${runner.temp}/.npmrcand exportsNPM_CONFIG_USERCONFIGto reference it. Without this, GitHub Actions workflows usingactions/setup-nodeto authenticate to private registries broke after upgrading to pnpm v11. PNPM-prefixed env vars andnpmrcAuthFilefrom the globalconfig.yamlcontinue to take precedence #11539.pnpm packnot bundling dependencies listed inbundleDependencies(orbundledDependencies). The npm-packlist upgrade in pnpm 11 changed its API to require the caller to pre-populate the dependency tree, which the wrapper was not doing —bundleDependencieswere silently dropped from the tarball #11519.SyntaxError: Invalid regular expression flagsinstead of printing a clear "requires Node.js v22.13" error when launched on an unsupported Node.js version. The Node.js version check inbin/pnpm.mjswas effectively dead code because the staticimportof the bundleddist/pnpm.mjswas hoisted by the ES module loader and parsed before the check could run #11546.pnpm --prefix=<dir> installoverwriting the existingpnpm-workspace.yamlin<dir>withset this to true or falseplaceholders. The renamed--prefixoption (which maps todir) was not honored when locating the workspace root, so the workspace manifest'sallowBuildssettings were not loaded into config and got clobbered when ignored builds were auto-populated #11535.pnpm publish --provenancefailing with a 422 from the registry when the package version contained semver build metadata (e.g.1.0.0-canary.0+abc1234). The+<build>segment is now stripped before packing so that the version embedded in the tarball, the metadata sent to the registry, and the sigstore provenance subject all agree #11518.Platinum Sponsors
Gold Sponsors
v11.0.8: pnpm 11.0.8Compare Source
Patch Changes
pnpm-lock.yamlwhen they cannot be derived from name+version+registry, even with the defaultlockfileIncludeTarballUrl: false. Without this,pnpm install --frozen-lockfilefrom an empty store fails withERR_PNPM_FETCH_404for packages on registries that serve tarballs from a non-standard path — most notably GitHub Packages (https://npm.pkg.github.com/download/<scope>/<name>/<version>/<hash>) and JSR.lockfileIncludeTarballUrl: truecontinues to force the URL into the lockfile for every package #11276.preversion,version, andpostversionlifecycle scripts forpnpm version.ERR_PNPM_BAD_TARBALL_SIZEwhen a registry serves tarballs with an end-to-endContent-Encoding(e.g.gzip). Tarballs are already compressed, so the fetcher now requests them withAccept-Encoding: identity(matching pnpm v10's effective behavior) and, as defense in depth against misbehaving servers, no longer enforces the strictContent-Lengthcheck when the response declares aContent-Encoding—Content-Lengthin that case refers to the encoded payload, not the decoded bytes the fetch implementation yields #11506.Platinum Sponsors
Gold Sponsors
v11.0.7: pnpm 11.0.7Compare Source
Patch Changes
Restore the execute bit on the
node-gypshims packed inside@pnpm/exe(dist/node-gyp-bin/node-gyp,dist/node-gyp-bin/node-gyp.cmd, anddist/node_modules/node-gyp/bin/node-gyp.js). Without this,pnpm/action-setup's standalone path (used on runners with Node.js < 22.13) failed any install whose lifecycle script invokednode-gyp rebuildwithsh: 1: node-gyp: Permission denied#11483.Fixed the
pn,pnpx, andpnxaliases failing in Git Bash / MSYS2 on Windows when pnpm was installed via@pnpm/exe(or afterpnpm self-update) #11486. Runningpnpx(orpnx) printed the cmd.exe banner and dropped the user into an interactive command prompt instead of runningpnpm dlx. Thebinfield rewrite on Windows was pointing those aliases at.cmdfiles; cmd-shim's Bash shim for a.cmdtarget wraps it inexec cmd /C ..., and MSYS2 mangles/Cinto a Windows path before cmd.exe sees it. The aliases are now.exehardlinks of the SEA binary, which detects which name it was launched as viaprocess.execPathand prependsdlxforpnpx/pnx.Fix
pnpm installrecreatingnode_modulesafterpnpm fetch.pnpm fetchrecords emptyhoistPatternandpublicHoistPatternin.modules.yaml; since v11 removed the explicit-config gate, the follow-up install treated those as a hoist-pattern change and purged the modules directory. The fetch step now flags the modules manifest withvirtualStoreOnly: trueso the next install skips the hoist-pattern comparison and completes the missing post-import linking in place #11488.Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
A new
gitHosted: truefield is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.Allow user-level preferences in the global
config.yaml. The following settings can now be set in~/.config/pnpm/config.yaml(or viapnpm config set --location global) instead of being restricted topnpm-workspace.yaml:agent,globalVirtualStoreDir,initPackageManager,initType,registrySupportsTimeField,scriptShell,shellEmulator,sideEffectsCache,sideEffectsCacheReadonly,stateDir,strictDepBuilds,trustPolicy,trustPolicyExclude,trustPolicyIgnoreAfter,updateNotifier,useStderr,verifyDepsBeforeRun,verifyStoreIntegrity,virtualStoreDir,virtualStoreDirMaxLength#11474.Make trusted publishing (OIDC) take precedence over a configured static
_authTokeninpnpm publish, mirroring the npm CLI's behavior. When OIDC succeeds, the OIDC-derived token overrides any pre-configured_authToken; when OIDC is not applicable (no CI environment, exchange fails, registry has no trusted publisher configured), the static token is used as a fallback. This applies on every package during recursive publish, so each workspace package independently attempts trusted publishing.Additionally, the
NPM_ID_TOKENenv var is now honored as a CI-agnostic injection point for an OIDC ID token. Previously OIDC was only attempted on GitHub Actions or GitLab; now any CI provider that exposes its own OIDC mechanism (e.g. CircleCI'sCIRCLE_OIDC_TOKEN_V2, Buildkite, etc.) can forward its token viaNPM_ID_TOKENand trusted publishing will work without pnpm needing to recognize the provider explicitly.--pm-on-fail=ignore(and other universal options like--loglevel,--reporter) is now honored when combined with--helpor--version. Previously the CLI argument parser short-circuited those flags before universal options were preserved, sopnpm audit --pm-on-fail=ignore --helpandpnpm --pm-on-fail=ignore --versionreported the strict packageManager mismatch instead of running the requested action #11487.Fix a regression where
pnpm --recursive --filter '!<pkg>' run/exec/test/addwould include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative--filterarguments are provided, matching the documented behavior. To include the root, pass--include-workspace-root#11341.Restore npm-CLI-compatible
--jsonstdout output forpnpm publish(#11476). pnpm 11 reimplemented publish natively (#10591) and inadvertently dropped the per-package JSON object that pnpm 10 emitted transitively via the npm CLI, silently breaking downstream tooling — most notablynx release publish, which parses stdout JSON to confirm success (nrwl/nx#35575). On success, the output is now:pnpm publish --json→ single object{ id, name, version, size, unpackedSize, shasum, integrity, filename, files, entryCount, bundled }, mirroringnpm publish --json.pnpm publish -r --json→ array of those objects, mirroringpnpm pack --json's shape choice.pnpm publish -r --report-summary→ existingpnpm-publish-summary.jsonenvelope{ publishedPackages: [...] }is preserved, but each entry is upgraded to the same per-package shape (additive —nameandversionare still present).pnpm config get @<scope>:registrynow reports the same URL thatpnpm publishand the resolvers actually use. Previously,config getonly consulted.npmrc, whilepublish/install used the merged map that includespnpm-workspace.yaml'sregistriesblock — so the two could diverge silently and a publish could go to the wrong registry #11492.Platinum Sponsors
Gold Sponsors
v11.0.6: pnpm 11.0.6Compare Source
Patch Changes
pnpm_config_npmrc_auth_fileandpnpm_config_userconfigenv vars not actually loading the custom.npmrc. The env vars were parsed and assigned to the resolved config, but only afterloadNpmrcConfighad already read the default~/.npmrc— so the custom file path was set but never read. The relevant env vars are now consulted before the user-level.npmrcis loaded #11465.pnpm-workspace.yamlwhen updating it. Existing keys keep their position, and new keys are inserted in alphabetical position when the existing keys are already sorted (with a leadingpackageskey allowed) or appended at the end otherwise.pnpm self-updateon installations originally set up by pnpm v10. v10 addedPNPM_HOMEdirectly to PATH and wrote apnpmbootstrap shim there. v11 setup writes shims underPNPM_HOME/bininstead, so when a v10 user upgrades to v11 the legacy shim atPNPM_HOMEkeeps pointing into the old.tools/<version>install —pnpm --versioncontinues to report the pre-update version even though the new version was installed underglobal/v11. Self-update now detects this layout, refreshes the legacy shims so the upgrade actually takes effect, and prints a hint suggestingpnpm setupto migrate PATH to the v11 layout. #11464.nodeLinker,hoistPattern) are present inconfig.yamland silently ignored. Previously these settings were dropped without any feedback, leaving users unsure why their global configuration had no effect. The warning suggests moving those settings to a project-levelpnpm-workspace.yaml, or sharing them across projects via config dependencies.overrideshas an invalid shape or contains a non-string value.readPackagedependency map fields, includingdevDependencies, and reject falsy non-object invalid values instead of silently accepting them.pnpm config,pnpm set, andpnpm getby toleratingconfigDependenciesinstall failures. For these commands, a failure to installconfigDependencies(for example because the registry auth token has not been written yet) is now logged at debug level and the command proceeds. All other commands still surface the install error #10684.allowBuildsas an install-state input and clear previously ignored builds when they are explicitly disallowed.catalog:protocol with thepnpm dlx/pnpxcommand, resulting in a catalog entry not found error.PNPM_CONFIG_*(uppercase) environment variables in addition topnpm_config_*. Previously, only the lowercase form was honored, so env vars renamed per the v11 migration guide (e.g.PNPM_CONFIG_USERCONFIG) silently had no effect on case-sensitive systems like macOS and Linux #11465.Platinum Sponsors
Gold Sponsors
v11.0.5: pnpm 11.0.5Compare Source
Patch Changes
Drop the
darwin-x64artifact from@pnpm/exeand from the GitHub release page. The Node.js SEA mechanismpnpm pack-appuses produces a binary that segfaults at startup on Intel Macs because of an upstream Node.js bug (nodejs/node#62893, tracked alongside #59553; the Node.js team has opted not to fix it on the grounds that x64 macOS is being phased out). Re-signing withcodesignorldiddoesn't help — the corruption is in LIEF's Mach-O surgery, before signing.Intel Mac users should install pnpm via
npm install -g pnpm(uses the system Node.js, no SEA), or stay on pnpm 10.x.@pnpm/exe's preinstall on Intel Mac now exits with a clear error pointing at these alternatives.Closes #11423.
pnpm dlx(andpnpx/pnx/pnpm create) now runs the same interactiveapprove-buildsprompt aspnpm add -gwhen the package being launched depends on transitive packages with install scripts. Previously, the v11strictDepBuildsdefault made dlx fail withERR_PNPM_IGNORED_BUILDSand required users to re-run with--allow-build=<pkg>for every offending dependency. dlx also now removes the partially-populated cache directory when the install fails, so a subsequent run starts clean instead of reusing a broken install whose builds were silently skipped #11444.72629fc: Fixpnpm -g ls --jsonandpnpm -g ls --parseableso they emit valid JSON and parseable output respectively, matching pnpm 10 behavior. Since the isolated global packages refactor in pnpm 11, the global list command had a custom path that always printed plain text and ignored--json/--parseable, which broke tools likenpm-check-updatesthat parse the JSON output #11440.pnpm -g ls --depth=<n>(with n > 0) now errors when more than one isolated global install would be involved, since each install has its own lockfile and merging their transitive trees would be incoherent. When the request can be narrowed to a single install group, the regularlistflow is used and the full dependency tree is shown.Fixed
pnpm publishto honorpublishConfig.registryfrompackage.jsonwhen publishing a single package. The native publish flow introduced in v11 was reading the registry from.npmrconly, ignoring the per-package override #11419.When
strictPeerDependenciesistrue, theERR_PNPM_PEER_DEP_ISSUESerror once again renders the peer dependency issues inline using the same format aspnpm peers check, so users (and CI tools like Renovate) can see what failed without runningpnpm peers checkseparately #11439.The
WARNand error code labels in pnpm's output now wrap in brackets ([WARN],[ERR_PNPM_FOO]). Previously the labels relied entirely on a colored background to stand out, which meant they blended into the surrounding text in terminals without color (e.g. whenNO_COLORis set or output is piped). The brackets are painted in the same color as the badge background, so they appear as ordinary padding in color-capable terminals — only the no-color rendering changes.Platinum Sponsors
Gold Sponsors
v11.0.4: pnpm 11.0.4Compare Source
Patch Changes
pnpm cinot reinstalling workspace packagenode_modulesdirectories after the clean step #11427.pnpm ciperforms a fresh install after the clean step.pnpm-lock.yamlduringpnpm cleanwhenlockfile: trueis configured inpnpm-workspace.yaml. The lockfile is only removed when the--lockfileoption is passed topnpm clean.pnpm self-update(with no version argument) no longer downgrades pnpm when the registry'slatestdist-tag points to an older release than the currently active version. Runpnpm self-update latestto force a downgrade #11418.minimumReleaseAgeStrictnow defaults totruewhenever the user explicitly setsminimumReleaseAge(viapnpm-workspace.yaml, the globalconfig.yaml, the CLI, orpnpm_config_*env vars).Platinum Sponsors
Gold Sponsors
v11.0.3: pnpm 11.0.3Compare Source
Patch Changes
node_modules/.bin#11412.ERR_PNPM_FETCH_404when installing a project whose lockfile depends on afile:tarball. The previous behavior dropped thetarballfield fromfile:and git-hosted resolutions whenlockfile-include-tarball-url=false(the default), even though those URLs cannot be reconstructed from the package name, version, and registry #11407.Platinum Sponsors
Gold Sponsors
v11.0.2: pnpm 11.0.2Compare Source
Patch Changes
ENOENTsymlink failure whenpnpm add -gtriggers the approve-builds prompt. The global add flow used to forward an absolutemodulesDir(<installDir>/node_modules) into the install run byapprove-builds. The install layer treatedmodulesDiras a path relative tolockfileDirand joined it again, producing a doubled path on Windows becausepath.joindoes not collapse an embedded absolute path. The hoist step then tried tomkdirand symlink under<installDir>\<installDir>\node_modules\.pnpm\node_modules\...and failed withENOENT#11403.packageManagerDependenciesgoing stale when pnpm is invoked through corepack. The lockfile sync (and thedevEngines.packageManagerversion check) previously ran only when pnpm was invoked directly; under corepack the entire block was skipped, so a stale entry would persist even after the running pnpm version changed. The lockfile sync now runs regardless of how pnpm was invoked, while the pnpm-managed version switch (onFail: 'download') remains skipped under corepack so it doesn't fight corepack's own version selection #11397.publishConfig.directorywhen packages publish from a generated directory #11239.os/cpuentries (e.g.["!win32"]) being incorrectly rejected whensupportedArchitecturesexpands to multiple platforms #11375.Platinum Sponsors
Gold Sponsors
v11.0.1: pnpm 11.0.1Compare Source
Patch Changes
pnpm runscripts.nullnamed catalogs in workspace manifests withInvalidWorkspaceManifestErrorinstead of crashing with a rawTypeError.pnpm sbomemittedNOASSERTION(SPDX) and omitted the distribution reference (CycloneDX) for git dependencies. Now emits the git URL with commit hash, e.g.git+https://github.com/user/repo.git#commit.pnpm self-updatenow keepspackage.json'spackageManageranddevEngines.packageManagerin sync. When the legacypackageManagerfield pins pnpm, both fields are rewritten to the new exact pnpm version on update —packageManagertopnpm@<version>(without an integrity hash), anddevEngines.packageManager.versionto the same exact<version>(dropping any range operator). When onlydevEngines.packageManageris declared, the existing range-preserving behavior is unchanged #11388.pnpm audit --fixso that the log output order matches the order written topnpm-workspace.yaml.packageManagerDependenciesentry whendevEngines.packageManagerdeclares a pnpm version that the lockfile no longer satisfies. Previously, the stale entry was kept even though the running pnpm matched the declared version, silently breaking the integrity record #11387.Platinum Sponsors
Gold Sponsors
v11.0.0: pnpm 11Compare Source
Highlights
Major
minimumReleaseAgedefaults to 1 day (newly published packages are not resolved for 24h) andblockExoticSubdepsdefaults totrue.allowBuildsreplaces the old build-dependency settings —onlyBuiltDependencies,onlyBuiltDependenciesFile,neverBuiltDependencies,ignoredBuiltDependencies, andignoreDepScriptshave been removed.pnpm add -ggets its own directory with its ownpackage.json,node_modules, and lockfile.pnpm publish,login,logout,view,deprecate,unpublish,dist-tag, andversionno longer delegate to the npm CLI, and the remaining npm passthrough commands now throw "not implemented".pnpm audituses npm's bulk advisories endpoint — the legacy/security/auditsendpoints are gone. CVE-based filtering has been replaced with GHSA-based filtering: migrateauditConfig.ignoreCvesentries toauditConfig.ignoreGhsas..npmrcis auth/registry only — all other settings must live inpnpm-workspace.yamlor the new globalconfig.yaml, and environment variables use thepnpm_config_*prefix.node@runtime:<version>no longer extracts the bundlednpm,npx, andcorepack, roughly halving the files pnpm has to hash, write, and link.Minor
pnpm ci,pnpm sbom,pnpm clean,pnpm peers check,pnpm runtime set,pnpm docs/home,pnpm ping,pnpm search,pnpm star/unstar/stars,pnpm whoami,pnpm with, andpnpm pack-app, pluspn/pnxshort aliases..pnpmfile.mjs, which takes priority over.pnpmfile.cjswhen present.pnpm audit --fix=updatefixes vulnerabilities by updating packages in the lockfile instead of adding overrides, andpnpm audit --fix --interactivelets you select which advisories to fix.pnpm pack-apppacks a CommonJS entry into a standalone executable for one or more target platforms using Node.js Single Executable Applications.Major Changes
Requirements
Security & Build Defaults
Changed default values:
optimisticRepeatInstallis nowtrue,verifyDepsBeforeRunis nowinstall,minimumReleaseAgeis now1440(1 day), andminimumReleaseAgeStrictisfalse. Newly published packages will not be resolved until they are at least 1 day old. This protects against supply chain attacks by giving the community time to detect and remove compromised versions. To opt out, setminimumReleaseAge: 0inpnpm-workspace.yaml#11158.strictDepBuildsistrueby default.blockExoticSubdepsistrueby default.Removed deprecated build dependency settings:
onlyBuiltDependencies,onlyBuiltDependenciesFile,neverBuiltDependencies,ignoredBuiltDependencies, andignoreDepScripts#11220.Use the
allowBuildssetting instead. It is a map where keys are package name patterns and values are booleans:truemeans the package is allowed to run build scriptsfalsemeans the package is explicitly denied from running build scriptsSame as before, by default, none of the packages in the dependencies are allowed to run scripts. If a package has postinstall scripts and it isn't declared in
allowBuilds, an error is printed.Before:
After:
Removed
allowNonAppliedPatchesin favor ofallowUnusedPatches.Removed
ignorePatchFailures; patch application failures now throw an error.Store
<algo>-<digest>). Using hex format improves performance since file paths in the content-addressable store use hex representation, eliminating base64-to-hex conversion during path lookups.package.jsonfrom the content-addressable store during resolution and installation. This reduces I/O and speeds up repeat installs #10473.$STORE/index/, package metadata is stored in a single SQLite database at$STORE/index.dbwith MessagePack-encoded values. This reduces filesystem syscall overhead, improves space efficiency for small metadata entries, and enables concurrent access via SQLite's WAL mode. Packages missing from the new index are re-fetched on demand #10500 #10826.Global Packages
Global installs (
pnpm add -g pkg) andpnxnow use the global virtual store by default. Packages are stored at{storeDir}/linksinstead of per-project.pnpmdirectories. This can be disabled by settingenableGlobalVirtualStore: false#10694.Isolated global packages. Each globally installed package (or group of packages installed together) now gets its own isolated installation directory with its own
package.json,node_modules/, and lockfile. This prevents global packages from interfering with each other through peer dependency conflicts, hoisting changes, or version resolution shifts.Key changes:
pnpm add -g <pkg>creates an isolated installation in{pnpmHomeDir}/global/v11/{hash}/pnpm remove -g <pkg>removes the entire installation group containing the packagepnpm update -g [pkg]re-installs packages in new isolated directoriespnpm list -gscans isolated directories to show all installed global packagespnpm install -g(no args) is no longer supported; usepnpm add -g <pkg>insteadGlobally installed binaries are now stored in a
binsubdirectory ofPNPM_HOMEinstead of directly inPNPM_HOME. This prevents internal directories likeglobal/andstore/from polluting shell autocompletion whenPNPM_HOMEis on PATH #10986. After upgrading, runpnpm setupto update your shell configuration.Breaking changes to
pnpm link:pnpm link <pkg-name>no longer resolves packages from the global store. Only relative or absolute paths are accepted. For example, usepnpm link ./fooinstead ofpnpm link foo.pnpm link --globalis removed. Usepnpm add -g .to register a local package's bins globally.pnpm link(no arguments) is removed. Usepnpm link <dir>with an explicit path instead.Configuration
pnpm no longer reads all settings from
.npmrc. Only auth and registry settings are read from.npmrcfiles. All other settings (likehoistPattern,nodeLinker,shamefullyHoist, etc.) must be configured inpnpm-workspace.yamlor the global~/.config/pnpm/config.yaml#11189.Network settings (
httpProxy,httpsProxy,noProxy,localAddress,strictSsl,gitShallowHosts) are now written toconfig.yaml(global) orpnpm-workspace.yaml(local) instead of.npmrc/auth.ini. They are still readable from.npmrcfor easier migration from the npm CLI #11209.pnpm no longer reads
npm_config_*environment variables. Usepnpm_config_*environment variables instead (e.g.,pnpm_config_registryinstead ofnpm_config_registry).pnpm no longer reads the npm global config at
$PREFIX/etc/npmrc.pnpm loginwrites auth tokens to~/.config/pnpm/auth.ini.New
registriessetting inpnpm-workspace.yaml:Auth tokens in
~/.npmrcstill work — pnpm continues to read~/.npmrcas a fallback for registry authentication. The newnpmrcAuthFilesetting can be used to point to a different file instead of~/.npmrc.Replace workspace project specific
.npmrcwithpackageConfigsinpnpm-workspace.yaml.A workspace manifest with
packageConfigslooks something like this:Or this:
pnpm no longer reads settings from the
pnpmfield ofpackage.json. Settings should be defined inpnpm-workspace.yaml#10086.pnpm config get(without--json) no longer prints INI formatted text. Instead, it prints JSON for objects and arrays, and raw strings for strings, numbers, booleans, and nulls.pnpm config get --jsonstill prints all types of values as JSON, as before.pnpm config get <array>now prints a JSON array.pnpm config listnow prints a JSON object instead of INI formatted text.pnpm config listandpnpm config get(without argument) now hide auth-related settings.pnpm config listandpnpm config get(without argument) now show top-level keys as camelCase. Exception: keys that start with@or//are preserved (their cases don't change).pnpm config getandpnpm config listno longer load non-camelCase options from the workspace manifest (pnpm-workspace.yaml).Removed Commands & npm Passthrough
pnpm no longer falls back to the npm CLI. Commands that were previously passed through to npm (
access,bugs,docs,edit,find,home,issues,owner,ping,prefix,profile,pkg,repo,search,set-script,star,stars,team,token,unstar,whoami,xmas) and their aliases (s,se) now throw a "not implemented" error, with a suggestion to use the npm CLI directly #10642. Other previously passed-through commands —view(info,show,v),login(adduser),logout,deprecate,unpublish,dist-tag, andversion— have been reimplemented natively in pnpm (see New Commands below).pnpm publishnow works without thenpmCLI.The One-time Password feature now reads from
PNPM_CONFIG_OTPinstead ofNPM_CONFIG_OTP:If the registry requests OTP and the user has not provided it via the
PNPM_CONFIG_OTPenvironment variable or the--otpflag, pnpm will prompt the user directly for an OTP code.If the registry requests web-based authentication, pnpm will print a scannable QR code along with the URL.
Since the new
pnpm publishno longer callsnpm publish, some undocumented features may have been unknowingly dropped. If you rely on a feature that is now gone, please open an issue at https://github.com/pnpm/pnpm/issues. In the meantime, you can usepnpm pack && npm publish *.tgzas a workaround.Removed the
pnpm servercommand #10463.Removed support for the
useNodeVersionandexecutionEnv.nodeVersionfields.devEngines.runtimeandengines.runtimeshould be used instead #10373.Removed support for
hooks.fetchers. We now have a new API for custom fetchers and resolvers via thefetchersfield ofpnpmfile.Lifecycle Scripts
npm_config_*environment variables from the pnpm config during lifecycle scripts. Only well-knownnpm_*env vars are now set, matching Yarn's behavior #11116.CLI Output
$ commandinstead of> pkg@version stage path\n> command, and shows project name and path only when running in a different directory. The$ commandline is printed to stderr to keep stdout clean for piping #11132.pnpm peers checkto view the issues #11133.Lockfile
patchedDependencieslockfile format fromRecord<string, { path: string, hash: string }>toRecord<string, string>(selector to hash). Existing lockfiles with the old format are automatically migrated #10911.Other
The default value of the
typefield in thepackage.jsonfile of the project initialized bypnpm initcommand has been changed tomodule.Added support for lowercase options in
pnpm add:-d,-p,-o,-e#9197.When using the
pnpm addcommand only:-pis now an alias for--save-prodinstead of--parseable-dis now an alias for--save-devinstead of--loglevel=infoThe root workspace project is no longer excluded when it is explicitly selected via a filter #10465.
Audit
pnpm auditnow calls npm's/-/npm/v1/security/advisories/bulkendpoint. The legacy/-/npm/v1/security/audits{,/quick}endpoints have been retired by the registry, so the legacy request/response contract is no longer supported.The bulk endpoint does not return CVE identifiers. CVE-based filtering has been replaced with GitHub advisory ID (GHSA) filtering:
auditConfig.ignoreCves→auditConfig.ignoreGhsas(the previous key is no longer recognized)pnpm audit --ignore <id>/pnpm audit --ignore-unfixablenow read and write GHSAs instead of CVEsurl(https://github.com/advisories/GHSA-xxxx-xxxx-xxxx)To migrate: replace each
CVE-YYYY-NNNNNentry in yourauditConfig.ignoreCveswith the correspondingGHSA-xxxx-xxxx-xxxxvalue (visible in theMore infocolumn ofpnpm auditoutput) and move it underauditConfig.ignoreGhsas.Package Manager Settings
Breaking: removed the
managePackageManagerVersions,packageManagerStrict, andpackageManagerStrictVersionsettings. They existed only to derive theonFailbehavior for the legacypackageManagerfield, and thepmOnFailsetting introduced alongsidepnpm withsubsumes all three — it directly sets theonFailbehavior of bothpackageManageranddevEngines.packageManager. TheCOREPACK_ENABLE_STRICTenvironment variable is no longer honored (it only gatedpackageManagerStrict); usepmOnFailinstead.Migration:
managePackageManagerVersions: truepmOnFail: download(default)managePackageManagerVersions: falsepmOnFail: ignorepackageManagerStrict: falsepmOnFail: warnpackageManagerStrictVersion: truepmOnFail: errorCOREPACK_ENABLE_STRICT=0pmOnFail: warnRuntime Installs
node@runtime:<version>(includingpnpm env useandpnpm runtime set node) no longer extracts the bundlednpm,npx, andcorepackfrom the Node.js archive. This cuts roughly half of the files pnpm has to hash, write to the CAS, and link during installation, making runtime installs noticeably faster. Users who still neednpmcan install it as a separate package.Minor Changes
New Commands
pnpm view(info,show,v) command for viewing package metadata from the registry #11064.pnpm login(andpnpm adduseralias) command for authenticating with npm registries. Supports web-based login with QR code as well as classic username/password login #11094.pnpm logoutcommand for logging out of npm registries. Revokes the authentication token on the registry and removes it from the local auth config file #11213.pnpm deprecateandpnpm undeprecatecommands for setting and removing deprecation messages on package versions without delegating to the npm CLI #11120.pnpm unpublishcommand. Supports unpublishing specific versions, version ranges via semver, and entire packages with--force#11128.pnpm dist-tagcommand (ls,add,rmsubcommands) #11218.pnpm sbomcommand for generating Software Bill of Materials in CycloneDX 1.7 and SPDX 2.3 JSON formats #9088.pnpm cleancommand that safely removesnode_modulesdirectories from all workspace projects #10707. Use--lockfileto also removepnpm-lock.yamlfiles.pnpm runtime set <runtime name> <runtime version spec> [-g]for installing runtimes. Deprecatedpnpm env usein favor of the new command.pnpm audit --fix=update#10341.pnpm cicommand for clean installs #6100. The command runspnpm cleanfollowed bypnpm install --frozen-lockfile. Designed for CI/CD environments where reproducible builds are critical. Aliases:pnpm clean-install,pnpm ic,pnpm install-clean#11003.pnpm peers checkcommand that checks for unmet and missing peer dependency issues by reading the lockfile #7087.versioncommand natively in pnpm to support workspaces andworkspace:protocols correctly. The new command allows bumping package versions (major, minor, patch, etc.) with full workspace support and git integration #10879.pnpm audit --fixnow supports a new interactive mode via--interactive/-i.pnpm docscommand and its aliaspnpm home. This command opens the package documentation or homepage in the browser. When the package has no valid homepage, it falls back tohttps://npmx.dev/package/<name>.pnpm pingcommand to test registry connectivity. Provides a simple way to verify connectivity to the configured registry without requiring external tools.searchcommand and its aliases (s,se,find).star,unstar,stars, andwhoamicommands.pnpm with <version|current> <args...>command. Runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project'spackageManageranddevEngines.packageManagerpins.pnpm pack-appcommand that packs a CommonJS entry file into a standalone executable for one or more target platforms, using the Node.js Single Executable Applications API under the hood.Configuration
Added support for a global YAML config file named
config.yaml.Configuration is now split into two categories:
rcfile and local.npmrc.config.yamland localpnpm-workspace.yaml.Added support for loading environment variables whose names start with
pnpm_config_into config. These environment variables override settings frompnpm-workspace.yamlbut not CLI arguments.Added support for reading
allowBuildsfrompnpm-workspace.yamlin the global package directory for global installs.Added support for
pnpm config get globalconfigto retrieve the global config file path #9977.Added a new setting
virtualStoreOnlythat populates the virtual store without creating importer symlinks, hoisting, bin links, or running lifecycle scripts. This is useful for pre-populating a store (e.g., in Nix builds) without creating unnecessary project-level artifacts.pnpm fetchnow uses this mode internally #10840.Added support for specifying the pnpm version via
devEngines.packageManagerinpackage.json. Unlike thepackageManagerfield, this supports version ranges. The resolved version is stored inpnpm-lock.yamland reused if it still satisfies the range #10932.Added a new
dedupePeerssetting that reduces peer dependency duplication. When enabled, peer dependency suffixes use version-only identifiers (name@version) instead of full dep paths, eliminating nested suffixes like(foo@1.0.0(bar@2.0.0)). This dramatically reduces the number of package instances in projects with many recursive peer dependencies #11070.Config dependencies are now installed into the global virtual store (
{storeDir}/links/) and symlinked intonode_modules/.pnpm-config/. This allows config dependencies to be shared across projects that use the same store, avoiding redundant fetches and imports #10910. Config dependency and package manager integrity info is now stored inpnpm-lock.yamlinstead of inlined inpnpm-workspace.yaml: the workspace manifest contains only clean version specifiers forconfigDependencies, while the resolved versions, integrity hashes, and tarball URLs are recorded in the lockfile as a separate YAML document. The env lockfile section also storespackageManagerDependenciesresolved during version switching and self-update. Projects using the old inline-hash format are automatically migrated on install #10912 #10964.Added
nodeDownloadMirrorssetting to configure custom Node.js download mirrors inpnpm-workspace.yaml. This replaces thenode-mirror:<channel>.npmrcsetting, which is no longer read #11194:pnpm dlxandpnpm createnow respect security and trust policy settings (minimumReleaseAge,minimumReleaseAgeExclude,minimumReleaseAgeStrict,trustPolicy,trustPolicyExclude,trustPolicyIgnoreAfter) from project-level configuration #11183.pnpm initnow writes adevEngines.packageManagerfield instead of thepackageManagerfield wheninit-package-manageris enabled.Added a new setting
runtimeOnFailthat overrides theonFailfield ofdevEngines.runtime(andengines.runtime) in the root project'spackage.json. Accepted values:ignore,warn,error,download. For example, settingruntimeOnFail=downloadmakes pnpm download the declared runtime version even when the manifest does not setonFail: "download".Added a new setting
minimumReleaseAgeIgnoreMissingTime, which istrueby default. When enabled, pnpm skips theminimumReleaseAgematurity check if the registry metadata does not include thetimefield. Set tofalseto fail resolution instead.Store
Hooks & Pnpmfiles
.mjsextension. When.pnpmfile.mjsexists, it takes priority over.pnpmfile.cjsand only one is loaded #9730.CLI & Other
clean,setup,deploy, andrebuildcommands now prefer user scripts over built-in commands. When a project'spackage.jsonhas a script with the same name,pnpmexecutes the script instead of the built-in command. Addedpurgeas an alias for the built-incleancommand, which always runs the built-in regardless of scripts #11118.-Fas a short alias for the--filteroption..are hidden and cannot be run directly viapnpm run. They can only be called from other scripts. Hidden scripts are also omitted from thepnpm runlisting #11041.pnpm approve-buildsnow accepts positional arguments for approving or denying packages without the interactive prompt. Prefix a package name with!to deny it. Only mentioned packages are affected; the rest are left untouched #11030.allowBuildsare automatically added topnpm-workspace.yamlwith a placeholder value, so users can manually set them totrueorfalse#11030.pnandpnxshort aliases forpnpmandpnpx(pnpm dlx) #11052.pnpm store prunenow displays the total size of removed files #11047.pnpm audit --fixnow adds the minimum patched version for each advisory tominimumReleaseAgeExcludeinpnpm-workspace.yaml, so the security fix can be installed without waiting forminimumReleaseAge#11216.optimisticRepeatInstallskipsshouldRefreshResolutionhooks #10995.Performance
node-fetchwith nativeundicifor HTTP requests throughout pnpm #10537.node_modules, avoiding the overhead of creating a temp dir and renaming per package #11088.gunzipSyncchunk size for fewer buffer allocations during tarball decompression #11086.If-Modified-Sincefor conditional metadata fetches, avoiding re-downloading unchanged registry metadata #11161.minimumReleaseAge, reducing the amount of data fetched from the registry #11160.Patch Changes
Switched to
process.stderr.writeinstead ofconsole.errorfor script logging #11140.Respected the
frozen-lockfileflag when migrating config dependencies #11067.Removed the
--workspaceflag from theversioncommand #11115.Handled
ENOTSUPerror in the clone import path during parallel I/O #11117.Fixed
pnpm auditcommand.Updated dependencies to fix vulnerabilities.
pnpm now checks whether a package is installable for non-npm-hosted packages (e.g., git or tarball dependencies) after the manifest has been fetched.
pnpm now explicitly passes the path of the global
rcconfig file tonpm.Fixed YAML formatting preservation in
pnpm-workspace.yamlwhen running commands likepnpm update. Previously, quotes and other formatting were lost even when catalog values didn't change.Closes #10425
The parameter set by the
--allow-buildflag is now written toallowBuilds.Fixed a bug in which specifying
filterinpnpm-workspace.yamlwould cause pnpm to not detect any projects.Deferred patch errors until all patches in a group are applied, so that one failed patch does not prevent other patches from being attempted.
pnpm now fails on incompatible lockfiles in CI when frozen lockfile mode is enabled #10978.
Fixed
strictDepBuildsandallowBuildschecks being bypassed when a package's build side-effects are cached in the store #11039.In GVS mode,
pnpm approve-buildsnow runs a full install instead of rebuild, ensuring that GVS hash directories and symlinks are updated correctly after changingallowBuilds#11043.Fixed a crash in the lockfile merger when merging non-semver version strings (e.g.
link:,file:, git URLs) #11102.Handled
ENOTSUPerror inlinkOrCopyduring parallel imports #11103.Skipped linking bins that already reference the correct target. This avoids redundant I/O during repeated installs and prevents permission errors when the store is read-only (e.g. Docker layer caching, CI prewarm, NFS) #11069.
Fixed
_passwordhandling for the default registry to decode from base64 before use, consistent with scoped registry behavior #11089.Fixed a bug where the CAS locker cache was not updated when a file already existed with correct integrity #11085.
Prevented catalog entries from being removed by
cleanupUnusedCatalogswhen they are referenced only from workspaceoverrides#11075.Resolved patch file paths during
pnpm fetch#11054.Fixed invalid specifiers for peers on all non-exact version selectors #11049.
Fixed false "Command not found" error on Windows when the command exists but exits with a non-zero exit code #11000.
Prepended
Bearerto the authorization token generated bytokenHelperif it is missing, aligning with npm's behavior #11097.Propagated error cause when throwing
PnpmErrorin@pnpm/npm-resolver#10990.Fixed SQLite race condition during store initialization on Windows.
Removed
rimrafSyncinimportIndexedDirfast-path error handler #11168.Fixed
pnpm dedupe --checkunexpectedly failing due to non-deterministic resolution #11110.Fixed empty files not being rejected in
isEmptyDirOrNothing#11182.Fixed
.bat/.cmdtoken helpers not working on Windows due to missingshell: trueoption.Platinum Sponsors
Gold Sponsors
v10.34.5: pnpm 10.34.5Compare Source
Patch Changes
78e29fe: Prevent a craftedpnpm-lock.yamlfrom writing package content outside the virtual store. A dependency path key whose name reconstructs to a path-traversal sequence (e.g.../../../tmp/x@1.0.0) is now rejected by the isolated (virtual-store) linker and the Plug'n'Play resolver map, matching the containment already applied to the hoisted linker. Under the global virtual store, a traversal in the version-derived path segment (e.g. a snapshotversion: "../../x") is now rejected atiterateHashedGraphNodes, the single point every global-virtual-store slot path funnels through.78e29fe: Fixed a path traversal vulnerability where a dependency whose manifestnamewas a scoped path traversal (e.g.@x/../../../<path>) could be written outsidenode_modulesto an attacker-controlled location duringpnpm install, even with--ignore-scripts. The isolated linker now validates the package name before using it as a directory name, matching the existing protection in the hoisted linker.47ef6f0: Fixed switching to and self-updating to pnpm v12. pnpm v12 (the Rust port) ships as thepnpmand@pnpm/exenpm packages whose bins are placeholders replaced at install time by the host's native binary from a@pnpm/exe.<platform>-<arch>[-musl]optional dependency. Because pnpm installs its own engine with--ignore-scripts, that relinking never ran, leaving a non-executable placeholder. pnpm now relinks the native binary itself for v12 (recognizing the new platform-package naming scheme and the nativepnpmpackage), and verifies the native binary's npm registry signature before running it.36928be:${...}environment-variable placeholders in thehttpProxy,httpsProxy,noProxy,proxy, andnoproxysettings are no longer expanded when these settings come from a project'spnpm-workspace.yaml. They now receive the same protection already applied toregistry.Platinum Sponsors
Gold Sponsors
v10.34.4: pnpm 10.34.4Compare Source
Patch Changes
352ae48: Security: validate config dependency names and versions before using them to build filesystem paths. Apnpm-workspace.yamlwith a traversal-shapedconfigDependenciesname (such as../../PWNED) or version (such as../../../PWNED) could previously causepnpm installto create symlinks or write package files outsidenode_modules/.pnpm-configand the store. Names must now be valid npm package names and versions must be exact semver versions. See GHSA-qrv3-253h-g69c.352ae48: Reject path-traversal and reserved dependency aliases (such as../../../escape,.bin,.pnpm, ornode_modules) that come from a lockfile rather than a freshly resolved manifest. A crafted lockfile alias could otherwise be joined directly under a hoistednode_modulesdirectory, letting package files be written outside the intended install root or overwrite pnpm-owned layout.The
nodeLinker: hoistedgraph builder now validates each alias at the directory sink (safeJoinModulesDir), matching the validation pnpm already performs when resolving aliases from manifests. See GHSA-fr4h-3cph-29xv.352ae48: Preventpnpm patch-removefrom removing files outside the configured patches directory.217fbe0: Hardened the warning printed when a project.npmrcuses environment variables in registry/auth settings: the suggestedpnpm config setcommand is now only included for keys made up of shell-inert characters. Because the key comes from a repository-controlled.npmrcand a shell expands$(...), backticks, and$VAReven inside double quotes, a crafted key could otherwise have turned the suggested copy-paste command into command execution.Platinum Sponsors
Gold Sponsors
v10.34.3: pnpm 10.34.3Compare Source
⚠️ Security fix — environment variables in a project
.npmrc(action may be required)Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands
${ENV_VAR}placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:.npmrc—registry,@scope:registry, proxy URLs, URL-scoped keys (//host/…), and credential values (_authToken,_auth,_password,username,tokenHelper,cert,key);pnpm-workspace.yaml.This release also closes a bypass where a project
.npmrccould setuserconfig,globalconfig, orprefixto make pnpm load a repo-supplied file as trusted config (via@pnpm/npm-conf@3.0.3).Environment variables are still expanded in trusted config: your user-level
~/.npmrc, the global config, CLI options, and environment config.If your authentication broke after upgrading, move the token out of the committed
.npmrc:Or keep the
${NPM_TOKEN}line but put it in your user-level~/.npmrcinstead of the repo. In GitHub Actions,actions/setup-nodewithregistry-urlalready writes a user-level.npmrc, soNODE_AUTH_TOKENkeeps working. For other CI where editing each pipeline is hard, setNPM_CONFIG_USERCONFIG=.npmrcin the CI environment to declare the project.npmrctrusted.See https://pnpm.io/npmrc for full migration details.
Patch Changes
.npmrcuses an environment variable in a registry/proxy URL or in registry credentials. The message now explains why the setting was ignored and how to migrate it to a trusted source — for example by runningpnpm config set "<key>" <value>to store it in the global config, or by keeping the${...}line in the user-level~/.npmrc— with a link to https://pnpm.io/npmrc..npmrccan no longer redirect which files pnpm loads as its trusted user and global configuration. Previously such a file could setuserconfig,globalconfig, orprefixto point at an attacker-supplied file shipped in the repository, and pnpm would load it as a trusted config source — bypassing the protection that prevents repository config from expanding environment variables into registry request destinations and credentials, and allowing it to settokenHelper. The user/global config file locations are now resolved only from trusted sources (CLI options, environment config, the npm builtin config, and defaults) before the project and workspace.npmrcfiles are read. Fixed by upgrading@pnpm/npm-confto3.0.3.Platinum Sponsors
Gold Sponsors
v10.34.2: pnpm 10.34.2Compare Source
⚠️ Security fix — environment variables in a project
.npmrc(action may be required)Following GHSA-3qhv-2rgh-x77r, pnpm no longer expands
${ENV_VAR}placeholders that come from a repository-controlled config file, because a malicious repository could otherwise use them to leak your environment secrets (npm tokens, CI job tokens, etc.) to an attacker-controlled registry during install. This applies to:.npmrc—registry,@scope:registry, proxy URLs, URL-scoped keys (//host/…), and credential values (_authToken,_auth,_password,username,tokenHelper,cert,key);pnpm-workspace.yaml.This release also closes a bypass where a project
.npmrccould setuserconfig,globalconfig, orprefixto make pnpm load a repo-supplied file as trusted config (via@pnpm/npm-conf@3.0.3).Environment variables are still expanded in trusted config: your user-level
~/.npmrc, the global config, CLI options, and environment config.If your authentication broke after upgrading, move the token out of the committed
.npmrc:Or keep the
${NPM_TOKEN}line but put it in your user-level~/.npmrcinstead of the repo. In GitHub Actions,actions/setup-nodewithregistry-urlalready writes a user-level.npmrc, soNODE_AUTH_TOKENkeeps working. For other CI where editing each pipeline is hard, setNPM_CONFIG_USERCONFIG=.npmrcin the CI environment to declare the project.npmrctrusted.See https://pnpm.io/npmrc for full migration details.
Patch Changes
packageManagerfield, the registry it fetches from (and the proxy/TLS settings used for that traffic) now come exclusively from trusted config sources — CLI options, env config, user and global.npmrc— defaulting to the public npm registry, instead of the repository's project/workspace settings.packageManagerfield (orpnpm self-update) makes pnpm download another pnpm version, the staged install is verified corepack-style: the integrity recorded in the staged lockfile must carry a valid npm registry signature for the exactname@version, validated against npm's public signing keys that ship embedded in the pnpm CLI. Verification fails closed — a tampered download, an unsigned package, or an unreachable registry refuses the version switch rather than running an unverified binary. It runs only when the wanted version is actually downloaded (a tools-directory cache miss), so repeated commands pay no extra network round trip..npmrcandpnpm-workspace.yaml) can no longer expand${...}placeholders in registry/proxy request destinations, URL-scoped keys, or registry credential values, preventing repository-controlled configuration from exfiltrating environment secrets through request URLs. Trusted user/global/CLI/env config keeps full env expansion, so existing token and registry setup flows continue to work.binnames ("",".","..", and scoped forms such as@scope/..) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted.onlyBuiltDependencies(andallowBuilds) entries can approve lifecycle scripts for git, git-hosted tarball, direct tarball, and local directory artifacts. To approve one of those artifacts explicitly, use its peer-suffix-free lockfile depPath as the key. Lockfile entries are now rejected when a registry-style dependency path (name@semver) is backed by a git, directory, or git-hosted tarball resolution (ERR_PNPM_RESOLUTION_SHAPE_MISMATCH), so the dependency path is a reliable artifact identity by the time scripts can run.SHASUMS256.txtagainst the Node.js release team's public keys (embedded in the pnpm CLI) before trusting its hashes. The Node.js download mirror is repository-configurable (node-mirror:<channel>in.npmrc), and the integrity check previously trusted aSHASUMS256.txtfetched from that same mirror — a circular check that a malicious mirror could satisfy with a tampered binary and matching hashes. A mirror that proxies the real signed SHASUMS keeps working unchanged. Only thereleasechannel publishes signed SHASUMS files, so pre-release channels (rc, nightly, …) remain unverified.Platinum Sponsors
Gold Sponsors
v10.34.1: pnpm 10.34.1Compare Source
Patch Changes
pnpm-lock.yamlentries whose remote tarballresolution:block is missing theintegrityfield. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that stripsintegrity:) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under--frozen-lockfile. pnpm now fails closed at lockfile-read time withERR_PNPM_MISSING_TARBALL_INTEGRITY. Git-hosted tarballs (gitHosted: trueor a URL on codeload.github.com / bitbucket.org / gitlab.com) andfile:tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.Platinum Sponsors
Gold Sponsors
v10.34.0: pnpm 10.34Compare Source
Minor Changes
Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously,
pnpm install(non-frozen) would logERR_PNPM_TARBALL_INTEGRITY, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.pnpm installnow exits withERR_PNPM_TARBALL_INTEGRITYand a hint pointing at the new opt-in flag.The only opt-in is
pnpm install --update-checksums— narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.--forceandpnpm updatedeliberately do not bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide.--frozen-lockfilebehavior is unchanged.--fix-lockfilekeeps its documented purpose (filling in missing lockfile entries) and is also not a bypass.Patch Changes
_authToken,_auth,username/_password,tokenHelper, inlinecert/key) to the registry declared in the same config source at load time, so a later layer overridingregistry=(workspace.npmrc,pnpm-workspace.yaml, CLI--registry) cannot redirect a credential or client certificate authored for a different host. A deprecation warning is emitted whenever an unscoped per-registry setting is encountered, naming the source and the URL it was pinned to. Reported by JUNYI LIU.minimumReleaseAgehandling when cached metadata is abbreviated. The npm registry returns abbreviated package metadata (without the per-versiontimefield) by default, which made the maturity check throwERR_PNPM_MISSING_TIMEwhenever cached abbreviated metadata was reused. pnpm now upgrades cached abbreviated metadata to the full document via a follow-up fetch whenminimumReleaseAgeis active, persists the upgrade to the on-disk cache so subsequent installs skip the extra fetch, and letsERR_PNPM_MISSING_TIMEfrom the cache fast-path fall through to the network fetch even under strict mode.commitfield is not a 40-character hexadecimal SHA before invokinggit. A malicious lockfile could otherwise smuggle a value such as--upload-pack=<command>throughgit fetch/git checkout, which on SSH or local-file transports executes the supplied command.diff --githeaders reference paths outside the patched package directory. Previously a malicious.patchfile added via a pull request could write, delete, or rename arbitrary files reachable by the user runningpnpm install.--prefix=<dir>not being honored when locating the workspace root. The--prefix → dirrename was applied after workspace detection, so workspace settings declared in<dir>/pnpm-workspace.yamlwere not loaded when pnpm was invoked from outside<dir>#11535.@x/../../../../../.git/hooks) when reading them from a package manifest or symlinking them intonode_modules. A malicious registry package could otherwise use a transitive dependency key to makepnpm installcreate symlinks at attacker-chosen paths outside the intendednode_modulesdirectory.Platinum Sponsors
Gold Sponsors
v10.33.4: pnpm 10.33.4Compare Source
Patch Changes
Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
A new
gitHosted: truefield is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.Fix a regression where
pnpm --recursive --filter '!<pkg>' run/exec/test/addwould include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative--filterarguments are provided, matching the documented behavior. To include the root, pass--include-workspace-root#11341.Platinum Sponsors
Gold Sponsors
v10.33.3: pnpm 10.33.3Compare Source
Patch Changes
@pnpm/exeto v11+ on Intel macOS (darwin-x64),pnpm self-updatenow transparently switches to the JS-onlypnpmpackage on npm instead of installing@pnpm/exe@v11+(which doesn't ship a working binary for Intel Macs because of an upstream Node.js SEA bug — see #11423 and nodejs/node#62893). Without this, the self-update would silently leave the user with no workingpnpmbinary. The new install requires Node.js to be available onPATH; a warning is printed when the swap happens. All other host/version combinations are unchanged.pnpm self-update(with no version argument) no longer downgrades pnpm when the registry'slatestdist-tag points to an older release than the currently active version. Runpnpm self-update latestto force a downgrade #11418.Platinum Sponsors
Gold Sponsors
v10.33.2: pnpm 10.33.2Compare Source
Patch Changes
Globally-installed bins no longer fail with
ERR_PNPM_NO_IMPORTER_MANIFEST_FOUNDwhen pnpm was installed via the standalone@pnpm/exebinary (e.g.curl -fsSL https://get.pnpm.io/install.sh | sh -) on a system without a separate Node.js installation. Previously, whenwhich('node')failed duringpnpm add --global, pnpm fell back toprocess.execPath, which in@pnpm/exeis the pnpm binary itself — and that path was baked into the generated bin shim, causing the shim to invoke pnpm instead of Node #11291, #4645.Fix an infinite fork-bomb that could happen when pnpm was installed with one version (e.g.
npm install -g pnpm@A) and run inside a project whosepackage.jsonselected a different pnpm version via thepackageManagerfield (e.g.pnpm@B), while apnpm-workspace.yamlalso existed at the project root.The child's environment is now forced to
manage-package-manager-versions=false(v10) andpm-on-fail=ignore(v11+), which disables the package-manager-version handling in whichever pnpm runs as the child.Fixes #11337.
Platinum Sponsors
Gold Sponsors
v10.33.1: pnpm 10.33.1Compare Source
Patch Changes
packageManagerfield selects pnpm v11 or newer, commands that v10 would have passed through to npm (version,login,logout,publish,unpublish,deprecate,dist-tag,docs,ping,search,star,stars,unstar,whoami, etc.) are now handed over to the wanted pnpm, which implements them natively. Previously they silently shelled out to npm — making, for example,pnpm version --helpprint npm's help on a project withpackageManager: pnpm@11.0.0-rc.3#11328.Platinum Sponsors
Gold Sponsors
microsoft/TypeScript (typescript)
v7.0.2: TypeScript 7.0.2Compare Source
https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/
This tag was originally released at: https://github.com/microsoft/typescript-go/releases/tag/typescript%2Fv7.0.2
v6.0.3: TypeScript 6.0.3Compare Source
For release
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 Immortal: This PR will be recreated if closed unmerged. Get config help if that's undesired.
This PR has been generated by Mend Renovate CLI.
fc664a6a2dtoc4c0202a48c4c0202a48to2275c060d52275c060d5todfdc488725dfdc488725to8bcd5d3e3e8bcd5d3e3etoa379cc70e1a379cc70e1to43aaacf1e043aaacf1e0tofd7a494552fd7a494552to426797335e426797335eto05bda0f89505bda0f895toec74dfbf61ec74dfbf61to20f4931345View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.