---
title: "pnpm 11.7 Adds `frozenStore` for Read-Only Filesystems, Lets pacquet Resolve Dependencies, and Closes a Lockfile Path-Traversal"
description: "pnpm 11.7.0 (June 15, 2026) ships four headline changes: a `--frozen-store` install mode for Nix stores, OCI layers, and other read-only mounts; delegation of dependency resolution to the pacquet Rust port (not just materialization); an opt-in `--batch` flag for `pnpm publish --recursive`; and a security fix that rejects path-traversal and reserved aliases (`.bin`, `.pnpm`, `node_modules`, `../../escape`) in lockfile-sourced dependencies."
date: 2026-06-17
image: "/images/heroes/2026-06-17--pnpm-11-7-frozen-store-publish-batch.png"
author: lschvn
tags: ["security", "tooling", "performance"]
tldr:
  - "pnpm 11.7.0 released June 15, 2026 with a new `--frozen-store` install mode that opens the package store's `index.db` with SQLite's `immutable=1` URI, suppressing every store-write path so `pnpm install` can run against a read-only filesystem (Nix store, OCI layer, read-only bind mount). Pairs with `--offline --frozen-lockfile` for fully reproducible installs. Requires Node.js 22.15+, 23.11+, or 24+."
  - "The pacquet Rust port of pnpm now handles dependency resolution end-to-end (not just materialization) on a non-frozen install, provided the installed pacquet is at least 0.11.7. Older pacquet releases keep the resolve-then-materialize split, and `add`/`update`/`remove` still resolve in pnpm because they have to mutate the manifests first."
  - "The release also closes a real lockfile attack surface: a crafted lockfile alias could be joined directly under a hoisted `node_modules` directory, letting package files be written outside the install root or overwrite pnpm-owned layout. The fix adds two layers of validation (in the `hoisted` graph builder and in the lockfile verification gate) that reject reserved aliases like `.bin`, `.pnpm`, `node_modules`, and any path-traversal pattern, regardless of node linker."
faq:
  - question: "What is new in pnpm 11.7?"
    answer: "pnpm 11.7.0 (June 15, 2026) ships a `--frozen-store` install mode for read-only filesystems (Nix stores, OCI layers, read-only bind mounts), lets the pacquet Rust port handle dependency resolution end-to-end on a non-frozen install, adds an opt-in `--batch` flag for `pnpm publish --recursive`, and closes a lockfile path-traversal vulnerability. It also fixes a Windows regression in `pnpm add`, a `pnpm patch-remove` path-scoping bug, and a deterministic child-resolution edge case."
  - question: "What is `frozenStore` and when do I want it?"
    answer: "`frozenStore` (the config key, also exposed as `--frozen-store` on the CLI) is a new install mode for read-only filesystems. The store's SQLite `index.db` is opened with the `immutable=1` URI, which bypasses the WAL/`-shm` sidecar creation that otherwise fails on a read-only directory. pnpm also suppresses every store-write path: the `index.db` writer, the project-registry write, the side-effects cache, and the `chmod` that makes a bin executable. The intent is fully reproducible installs where the store is shipped as a precomputed artifact (Nix store, OCI image layer, read-only bind mount) and must not be mutated during install."
  - question: "What is the Node.js requirement for `--frozen-store`?"
    answer: "The `immutable=1` URI requires Node.js 22.15.0, 23.11.0, or 24.0.0 or later. On older runtimes, `--frozen-store` fails fast with `ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE` rather than silently opening the store in a way that mutates the WAL. The other --frozen-* flags (--offline, --frozen-lockfile) work the same as on a regular install."
  - question: "How does pacquet resolution delegation work?"
    answer: "When pacquet is declared in `configDependencies` and the installed version is at least 0.11.7, a default (non-frozen, isolated `nodeLinker`) `pnpm install` delegates both resolution and materialization to pacquet in a single pass: pacquet reads the manifests, writes `pnpm-lock.yaml`, and creates `node_modules`. The lockfile format stays the same. Older pacquet releases only handled the materialization half, and `pnpm add`, `pnpm update`, and `pnpm remove` still resolve in pnpm (they have to mutate the manifests first, then pacquet materializes). Detection is automatic from the installed pacquet's version."
  - question: "What was the lockfile path-traversal vulnerability?"
    answer: "Before 11.7, an attacker who could control a lockfile entry could set a dependency alias to a path-traversal string (`../../../escape`) or a reserved name (`.bin`, `.pnpm`, `node_modules`). Under `nodeLinker: hoisted`, the alias was joined directly under `node_modules`, letting package files be written outside the install root or overwrite pnpm-owned layout. The 11.7 fix adds two layers: the `hoisted` graph builder now validates each alias at the directory sink (`safeJoinModulesDir`), matching the validation pnpm already performed for manifest-sourced aliases, and the lockfile verification gate (`verifyLockfileResolutions`) runs an always-on 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. The verification gate runs for every node linker at once."
  - question: "Does pnpm 11.7 work with my existing lockfile?"
    answer: "Yes. The lockfile format is unchanged in 11.7. The new lockfile verification gate runs against the existing lockfile on every install and rejects any entry that fails the alias check, but the format itself does not change. The only practical effect for an existing project is that an entry with a path-traversal or reserved alias (which would be a previously-crafted attack, not a normal entry) now fails the install with a clear error instead of being silently applied."
  - question: "What does the new `--batch` flag do for `pnpm publish --recursive`?"
    answer: "The `--batch` flag is an opt-in 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."
  - question: "Is pnpm 11.7 a breaking change for normal projects?"
    answer: "No. The lockfile format is unchanged, the new alias validation runs against existing lockfiles (and only rejects entries that should never have been there), `frozenStore` is opt-in, pacquet resolution delegation is opt-in (only kicks in when pacquet is declared in `configDependencies`), and the `--batch` flag is opt-in. The only breaking change in 11.7 is a fix to the Windows `pnpm add` regression introduced in 11.6.0 (`Cannot destructure property 'manifest'`) that was already fixed in 11.7."
---

[pnpm 11.7.0](https://github.com/pnpm/pnpm/releases/tag/v11.7.0) shipped on June 15, 2026, four days after the [11.6.0 security release that fixed the `.npmrc` environment-variable expansion vulnerability (GHSA-3qhv-2rgh-x77r)](https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r). The 11.7 line picks up where 11.6 left off on the supply-chain hardening story and adds three features that change how teams run pnpm in containerized and reproducible-build environments: a `--frozen-store` install mode for read-only filesystems, full dependency-resolution delegation to the pacquet Rust port, and an opt-in batch mode for `pnpm publish --recursive`. There is also a real lockfile security fix that closes a path-traversal edge case the [June 2026 npm supply chain attack retrospective](/articles/2026-06-06-npm-supply-chain-attack-red-hat-mini-shai-hulud) flagged as a recurring class of bug across the ecosystem.

## `frozenStore`: installs against a read-only package store

The headline feature of 11.7 is `frozenStore` (config key) and `--frozen-store` (CLI flag), an install mode for environments where the package store is on a read-only filesystem: a Nix store, an OCI image layer, a read-only bind mount, or a `dm-verity` rootfs. The store's SQLite `index.db` is opened with the [`immutable=1` URI](https://www.sqlite.org/uri.html), which bypasses the WAL/`-shm` sidecar creation that would otherwise fail with `EROFS` on a read-only directory. Every store-write path is suppressed: the `index.db` writer, the project-registry write, the side-effects cache, and the `chmod` that normally makes a bin executable when it crosses a read/write boundary.

The intended pairing is `--offline --frozen-lockfile --frozen-store` against a fully-populated store. Under the global virtual store (the default since 9.x), 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 pnpm patch applied), pnpm fails up front with `ERR_PNPM_FROZEN_STORE_NEEDS_BUILD` rather than crashing mid-build on a read-only write. If the store is missing its content directory entirely, the install fails fast with `ERR_PNPM_FROZEN_STORE_INCOMPLETE` rather than attempting to initialize it.

There are two hard constraints to be aware of. The `immutable=1` URI requires Node.js 22.15.0, 23.11.0, or 24.0.0 or later; on older runtimes, `--frozen-store` fails with a clear `ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE` error. And `--frozen-store` is incompatible with `--force` and with a configured pnpr server, since both write into the store. 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, pnpm now skips the `chmod` when the bin source is already executable and has a normalized shebang, and otherwise still errors out. The `chmod` is redundant when the seed already ships its bins executable.

The result is a fully reproducible install that can be cached as a single artifact and reused across CI, local dev, and production without any write access to the store. For Nix and OCI users, this is the missing piece: pnpm 11.6 was already usable in those environments, but every install would attempt a WAL `chmod` or a sidecar `shm` write that would either fail or fall back to a slower code path. 11.7 makes the read-only case the explicit, supported mode.

## pacquet now resolves dependencies, not just materializes

The second feature is a milestone for the [pacquet Rust port of pnpm](https://github.com/pnpm/pnpm/tree/main/pacquet): dependency resolution joins materialization in the set of operations pacquet can do end-to-end. The new behavior is opt-in via `configDependencies`: when pacquet is declared in `configDependencies` and the installed pacquet is at least 0.11.7, a default non-frozen install (isolated `nodeLinker`, plain `pnpm install`) is delegated to pacquet in a single pass. pacquet reads the manifests, writes `pnpm-lock.yaml`, and creates `node_modules`. pnpm detects the capability from the installed pacquet's version; older releases keep the resolve-then-materialize split.

`pnpm add`, `pnpm update`, and `pnpm remove` still resolve in pnpm itself, because those commands have to mutate the manifests before any resolution can happen. After the manifest is mutated, pacquet materializes. The lockfile format does not change. This remains an opt-in preview of the Rust install engine, tracked under [#11723](https://github.com/pnpm/pnpm/issues/11723). For projects already running pacquet in frozen-install mode, the change is invisible: the resolution and materialization are already a single pacquet invocation. For projects still running on the Node.js install engine, the upgrade is a no-op as long as pacquet is not in `configDependencies`.

## Lockfile path-traversal and reserved alias rejection

The third headline item is a security fix in the lockfile verifier. Before 11.7, an attacker who could control a lockfile entry (a malicious postinstall, a tampered CI artifact, a poisoned peer dependency's resolved spec) could set a dependency alias to a path-traversal string like `../../../escape` or to a reserved name like `.bin`, `.pnpm`, or `node_modules`. Under `nodeLinker: hoisted`, the alias was joined directly under `node_modules`, letting package files be written outside the install root or overwrite pnpm-owned layout. The exploit class is the same one the [esbuild 0.28.1 Windows path traversal (GHSA-g7r4-m6w7-qqqr)](/articles/2026-06-14-esbuild-0-28-1-deno-rce-windows-path-traversal) and the [March 2026 Axios npm supply chain attack](/articles/2026-03-31-axios-npm-supply-chain-attack) each surfaced in a different tool.

The 11.7 fix adds two layers. The `hoisted` graph builder now validates each alias at the directory sink (`safeJoinModulesDir`), matching the validation pnpm already performs for manifest-sourced aliases. And the lockfile verification gate (`verifyLockfileResolutions`) 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. The check is conservative: any alias that is not a syntactically valid package name (no `/`, no `..`, no reserved segments) is rejected with a clear error.

For a normal project, the practical effect is that an existing lockfile continues to install as before, and a lockfile with a path-traversal or reserved alias (which would be a previously-crafted attack, not a normal entry) now fails the install with a clear error. The fix does not change the lockfile format. It is the kind of defense-in-depth that the 11.6.0 `.npmrc` advisory also embodied: a project that already trusts its lockfile source still benefits from the verifier, because the verifier protects against bugs in the lockfile generator and against partial corruption.

## `--batch` for `pnpm publish --recursive`

The fourth feature is a small but practical one: `pnpm publish --recursive --batch` 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. For monorepos that publish N packages per release, the wall-clock win on `pnpm publish --recursive` is roughly Nx.

## Other fixes that affect day-to-day work

The 11.7 release also fixes several correctness and regression bugs that have been open since 11.6. The most notable is a Windows regression in `pnpm add` that produced `Cannot destructure property 'manifest' of 'manifestsByPath[rootDir]' as it is undefined` when running outside a workspace; the root cause was `selectProjectByDir` keying the `ProjectsGraph` by `opts.dir` instead of `project.rootDir`, so downstream `manifestsByPath` lookups missed when the two paths normalized differently (typically drive-letter casing). The `pnpm patch-remove` command also no longer removes files outside the configured patches directory. `pnpm publish` now respects `strictSsl: false` for self-signed certificates the same way `pnpm install` does. Git dependencies that point to a subdirectory of a repository (`repo#commit&path:/sub/dir`) keep their `path` in the lockfile again after an integrity-pin regression in 11.6. And the shared package child resolution is now deterministic when the same package is reached through multiple contexts, fixing a class of "missing peer" reports ([#12358](https://github.com/pnpm/pnpm/issues/12358)) where request timing decided the child context.

The `pnpm update -i` and `pnpm audit --fix -i` interactive prompts also got a UX fix: the summary line after pressing Enter used to print every selected choice's full table row (label, current/target versions, workspace, URL) joined by commas, producing a wall of text. The summary now lists only the selected package names (or vulnerability keys). This is a small thing, but it is the kind of polish that separates a 11.7 minor release from a 11.6 patch.
