Migrate to pnpm 11
Auditable, self-contained migration skills for upgrading JS/TS projects to pnpm 11 with supply-chain-hardened defaults. For Claude Code and Codex CLI.
npx -y skills add llbbl/pnpm-upgrade-skills --skill migrate-to-pnpm-11Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Migrate a JavaScript/TypeScript project from npm, yarn, or older pnpm (≤10) to pnpm 11, adopting its supply-chain-hardened defaults (minimumReleaseAge, allowBuilds, blockExoticSubdeps, strictDepBuilds, verifyDepsBeforeRun). Use when the user says "migrate to pnpm 11", "upgrade pnpm", "switch to pnpm", or wants to harden a project against npm supply-chain attacks.
SKILL.md
24.9 KB, as published. Nobody here has run it
/migrate-to-pnpm-11
Migrate a project to pnpm 11 and adopt its supply-chain security defaults.
Why pnpm 11
pnpm 11 (released 2026-04-28) turns on five supply-chain defaults that were opt-in or absent before:
| Setting | Default in v11 | Purpose |
|---|---|---|
minimumReleaseAge | 1440 (24 h) | Refuse to install package versions younger than the cooldown window. Blocks the typical "publish-malware → wait for CI to pull it" window. |
blockExoticSubdeps | true | Transitive deps cannot come from git URLs or arbitrary tarballs. |
strictDepBuilds | true | Any dependency with a postinstall/build script that is not on the allowBuilds allowlist fails the install with ERR_PNPM_IGNORED_BUILDS. |
verifyDepsBeforeRun | install | pnpm run <script> re-checks the lockfile before running. |
optimisticRepeatInstall | true | Skip work when the lockfile is unchanged. |
Plus: lockfile entries are re-validated against minimumReleaseAge even if the lockfile was committed under a weaker policy, Git-hosted deps get integrity pinning, and pnpm audit signatures verifies ECDSA registry signatures.
Prerequisites
- Node.js ≥ 22. pnpm 11 is pure ESM and drops Node 18/20.
- A clean working tree (or be willing to commit/stash before migrating — the migration touches lockfiles,
package.json,.npmrc, and may createpnpm-workspace.yaml).
Git workflow
This skill does not run git commit, git push, or gh pr directly. After the migration is verified, summarize the changes for the user and let them (or a separate commit/PR workflow) handle the commit.
Before editing any files, create a feature branch. The migration touches lockfiles, package.json, .npmrc, pnpm-workspace.yaml, the Dockerfile, and CI configs — too much for a stray commit on main.
git status # confirm clean tree
git checkout -b chore/migrate-to-pnpm-11
If the working tree isn't clean, stop and ask the user how to proceed (stash, commit elsewhere, or abort). Do not migrate on top of unrelated in-progress work.
Suggested commit/PR title: chore: migrate to pnpm 11 with supply-chain defaults.
Workflow
Step 1 — Detect the starting point
Run these in parallel and capture each result. Do not assume — check.
node -v
corepack -v 2>/dev/null || echo "no corepack"
cat package.json | grep -E '"packageManager"|"engines"' || true
ls -1 package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null
ls -1 pnpm-workspace.yaml .npmrc 2>/dev/null
From the output, classify the project as one of:
- npm —
package-lock.jsonexists, no pnpm/yarn lockfile. - yarn (classic / berry) —
yarn.lockexists. Check.yarnrc.ymlto tell berry from classic. - pnpm < 10 —
pnpm-lock.yamlwithlockfileVersion < 9, orpackageManager: pnpm@<10. - pnpm 10 —
lockfileVersion: '9.0'andpackageManager: [email protected].
Also detect: workspace (pnpm-workspace.yaml with packages:, or package.json workspaces), monorepo tooling (Turborepo, Nx, Changesets), and CI files that pin a package manager (.github/workflows/*.yml, Dockerfile, .tool-versions, .nvmrc).
If Node < 22: stop and tell the user. Suggest nvm install 22 && nvm use 22 (or mise/asdf equivalents). Update .nvmrc / .tool-versions / engines.node as part of the migration.
Step 2 — Install pnpm 11 via corepack
Prefer corepack so the version is pinned per-project rather than globally.
corepack enable
corepack prepare pnpm@latest --activate
pnpm -v # confirm 11.x
Then pin in package.json:
{
"packageManager": "[email protected]",
"engines": { "node": ">=22" }
}
Use the exact installed version (pnpm -v) rather than latest.
Step 3 — Convert the lockfile (path-specific)
Run from the repo root. For workspaces, run at the workspace root only.
From npm:
pnpm import # reads package-lock.json → writes pnpm-lock.yaml
rm package-lock.json
rm -rf node_modules
From yarn (classic or berry):
pnpm import # reads yarn.lock → writes pnpm-lock.yaml
rm yarn.lock .yarnrc .yarnrc.yml 2>/dev/null
rm -rf node_modules .yarn .pnp.* 2>/dev/null
From pnpm < 10: delete the old lockfile so pnpm 11 regenerates at lockfile v9. Do not carry forward a v6/v7 lockfile.
rm pnpm-lock.yaml
rm -rf node_modules
From pnpm 10: keep the lockfile as-is. Lockfile v9 is unchanged between 10 and 11.
Step 4 — Migrate configuration files
This is where the most subtle breakage happens. .npmrc is auth/registry only in pnpm 11. Every pnpm-specific setting moves into pnpm-workspace.yaml (even for non-workspace projects — the file is required as the settings home). The pnpm field in package.json is also no longer read.
Read the project's current .npmrc, pnpm-workspace.yaml, and package.json's pnpm field. Move every non-auth, non-registry key into pnpm-workspace.yaml.
| Old location | Old key | New location | New key |
|---|---|---|---|
.npmrc or package.json#pnpm | only-built-dependencies / onlyBuiltDependencies | pnpm-workspace.yaml | allowBuilds (allowlist map) |
| same | onlyBuiltDependenciesFile | same | allowBuilds (inline the file's contents) |
| same | neverBuiltDependencies | same | allowBuilds (omit entries, or set to false) |
| same | ignoredBuiltDependencies | same | allowBuilds (set to false) |
| same | ignoreDepScripts | same | allowBuilds: {} (empty allowlist = block all) |
| same | allowNonAppliedPatches | same | allowUnusedPatches |
| same | ignorePatchFailures | same | removed — no replacement; fix the patch |
package.json#pnpm.auditConfig | ignoreCves | pnpm-workspace.yaml | auditConfig.ignoreGhsas (translate CVE→GHSA via GitHub Advisory DB) |
| Env vars | npm_config_* | env / CI | pnpm_config_* |
Stay in .npmrc (do not move): registry=, //registry.npmjs.org/:_authToken=..., @scope:registry=, network proxy keys, engine-strict. Most other things move.
Step 5 — Write a hardened pnpm-workspace.yaml
Create or update the file. Keep defaults explicit so they survive future pnpm changes and so the security posture is visible in code review.
# Supply-chain hardening (defaults shown explicitly)
minimumReleaseAge: 1440 # block packages < 24 h old
blockExoticSubdeps: true # no git/tarball transitive deps
strictDepBuilds: true # fail install on unapproved postinstalls
verifyDepsBeforeRun: install # re-check lockfile before `pnpm run`
# Postinstall allowlist — explicitly approve every dep with a build script.
# Empty map = block all. Add entries only after auditing the script.
allowBuilds:
# esbuild: true
# \"@swc/core\": true
# If this is a workspace:
# packages:
# - 'packages/*'
# - 'apps/*'
For projects that legitimately need fresher releases (e.g. internal monorepo packages published moments before install), prefer raising minimumReleaseAge for the registry and using a per-package override rather than disabling it globally.
Step 6 — First install, expect ERR_PNPM_IGNORED_BUILDS
pnpm install
With strictDepBuilds: true and an empty allowBuilds, the install will fail listing every dependency that wants to run a build script. For each one:
- Look up the package on npm — what does its postinstall actually do?
- If it is a known legitimate native build (e.g.
esbuild,@swc/core,sharp,better-sqlite3,node-gyp-based deps the project actually uses), add it toallowBuildsastrue. - If it looks unnecessary or suspicious, leave it blocked (
falseor omitted) — the package will still install, just without running its script. Verify the app still works.
Re-run pnpm install until it succeeds.
Step 7 — Verify
pnpm install --frozen-lockfile # reproducibility check
pnpm audit # known advisories
pnpm audit signatures # ECDSA registry signatures (new in v11)
pnpm run build && pnpm test # or whatever the project uses
If pnpm audit signatures reports unsigned packages, surface them to the user but do not auto-pin around them.
Step 8 — Update CI, Docker, and tooling
Every CI provider has its own Node-install task and its own cache idiom. Don't generalize from the GitHub Actions example to GitLab or vice versa — follow the matching subsection below. Do not invent CI config from scratch; copy the snippet, adapt the names.
Three things to keep in mind across all providers:
- Node ≥ 22 in every place Node is pinned (CI config, Dockerfile,
.nvmrc,engines.node). - Cache the pnpm store, not
node_modules. pnpm uses a content-addressed store;node_modulesis just symlinks into it, so cachingnode_modulescaches the symlinks rather than the content. Key the cache onpnpm-lock.yaml's hash. - Install pnpm before using it. Corepack (shipped with Node since 16) is the simplest path —
corepack enable && corepack prepare pnpm@11 --activate. ThepackageManagerfield inpackage.jsonthen pins the exact version.
8a. Detect what's in use
# CI provider files
ls -1 \
.github/workflows \
.gitlab-ci.yml \
.circleci/config.yml \
bitbucket-pipelines.yml \
azure-pipelines.yml \
Jenkinsfile \
.drone.yml \
.buildkite/pipeline.yml .buildkite/pipeline.yaml \
.travis.yml \
2>/dev/null
# Container / dev-env files
ls -1 \
Dockerfile Dockerfile.* \
docker-compose.yml docker-compose.yaml compose.yml compose.yaml \
.dockerignore \
.devcontainer/devcontainer.json .devcontainer.json \
2>/dev/null
# Anything still referencing npm / yarn / pre-v11 pnpm / pre-22 Node
grep -RnE "npm (ci|install|run)\b|yarn (install|add|run)\b|pnpm@(7|8|9|10)\b|node:(16|18|20)\b|setup-node@v[0-3]\b" \
.github .gitlab-ci.yml .circleci bitbucket-pipelines.yml azure-pipelines.yml \
Jenkinsfile .drone.yml .buildkite .travis.yml \
Dockerfile* docker-compose*.yml compose*.yml \
Makefile justfile .tool-versions .nvmrc package.json \
2>/dev/null
8b. GitHub Actions
Replace actions/setup-node blocks with the following. pnpm/action-setup must run before actions/setup-node so cache: pnpm can find the binary.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm audit signatures
- run: pnpm test
Corepack-only alternative (no third-party action): replace the pnpm/action-setup step with - run: corepack enable before setup-node. The packageManager field in package.json then dictates the version.
8c. GitLab CI
Cache the store, not node_modules. The cache key auto-invalidates on lockfile changes.
# .gitlab-ci.yml
image: node:22
variables:
PNPM_HOME: "${CI_PROJECT_DIR}/.pnpm-store"
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
before_script:
- corepack enable
- corepack prepare pnpm@11 --activate
- pnpm config set store-dir "$PNPM_HOME"
test:
script:
- pnpm install --frozen-lockfile
- pnpm audit signatures
- pnpm test
8d. CircleCI
# .circleci/config.yml
version: 2.1
jobs:
test:
docker:
- image: cimg/node:22.13
steps:
- checkout
- run:
name: Enable corepack + pnpm
command: |
sudo corepack enable
corepack prepare pnpm@11 --activate
pnpm config set store-dir ~/.pnpm-store
- restore_cache:
keys:
- v1-pnpm-store-{{ checksum "pnpm-lock.yaml" }}
- v1-pnpm-store-
- run: pnpm install --frozen-lockfile
- save_cache:
key: v1-pnpm-store-{{ checksum "pnpm-lock.yaml" }}
paths:
- ~/.pnpm-store
- run: pnpm audit signatures
- run: pnpm test
The fallback cache key (v1-pnpm-store- with no checksum) restores the most recent store when the exact lockfile hash isn't cached; pnpm still verifies content hashes.
8e. Bitbucket Pipelines
The built-in node cache targets node_modules (wrong for pnpm). Define a custom store cache.
# bitbucket-pipelines.yml
image: node:22
definitions:
caches:
pnpm-store: $HOME/.pnpm-store
pipelines:
default:
- step:
caches: [pnpm-store]
script:
- corepack enable
- corepack prepare pnpm@11 --activate
- pnpm config set store-dir $HOME/.pnpm-store
- pnpm install --frozen-lockfile
- pnpm audit signatures
- pnpm test
8f. Azure Pipelines
# azure-pipelines.yml
trigger: [main]
pool:
vmImage: ubuntu-latest
variables:
PNPM_STORE_PATH: $(Pipeline.Workspace)/.pnpm-store
steps:
- task: NodeTool@0
inputs:
versionSpec: '22.x'
- script: |
corepack enable
corepack prepare pnpm@11 --activate
pnpm config set store-dir $(PNPM_STORE_PATH)
displayName: Install pnpm 11
- task: Cache@2
inputs:
key: 'pnpm | "$(Agent.OS)" | pnpm-lock.yaml'
path: $(PNPM_STORE_PATH)
displayName: Cache pnpm store
- script: pnpm install --frozen-lockfile
- script: pnpm audit signatures
- script: pnpm test
8g. Jenkinsfile (declarative)
The named Docker volume persists the store across builds on the same agent.
pipeline {
agent {
docker {
image 'node:22'
args '-v pnpm-store:/root/.local/share/pnpm/store'
}
}
environment {
COREPACK_ENABLE_DOWNLOAD_PROMPT = '0'
}
stages {
stage('Setup pnpm') {
steps {
sh '''
corepack enable
corepack prepare pnpm@11 --activate
pnpm -v
'''
}
}
stage('Install') { steps { sh 'pnpm install --frozen-lockfile' } }
stage('Verify') { steps { sh 'pnpm audit signatures' } }
stage('Test') { steps { sh 'pnpm test' } }
}
}
8h. Drone CI
# .drone.yml
kind: pipeline
type: docker
name: default
steps:
- name: test
image: node:22
volumes:
- name: pnpm-store
path: /root/.local/share/pnpm/store
commands:
- corepack enable
- corepack prepare pnpm@11 --activate
- pnpm install --frozen-lockfile
- pnpm audit signatures
- pnpm test
volumes:
- name: pnpm-store
host: { path: /tmp/pnpm-store }
8i. Buildkite
# .buildkite/pipeline.yml
steps:
- label: ":pnpm: test"
plugins:
- docker#v5.11.0:
image: node:22
mount-buildkite-agent: false
volumes:
- "buildkite-pnpm-store:/root/.local/share/pnpm/store"
commands:
- corepack enable
- corepack prepare pnpm@11 --activate
- pnpm install --frozen-lockfile
- pnpm audit signatures
- pnpm test
8j. Travis CI
Travis is in maintenance mode for most projects; if this migration is also a good moment to move to another provider, take it. If staying:
# .travis.yml
language: node_js
node_js: 22
cache:
directories:
- $HOME/.pnpm-store
before_install:
- corepack enable
- corepack prepare pnpm@11 --activate
- pnpm config set store-dir $HOME/.pnpm-store
install: pnpm install --frozen-lockfile
script:
- pnpm audit signatures
- pnpm test
8k. Dockerfile — patch existing first, then consider the patterns
Patch existing Dockerfiles before adding new ones. The most common post-migration Docker failure looks like this:
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: <pkg>@x.y.z, ...
Build Failed: ... exit code: 1
Root cause: pnpm-workspace.yaml is where pnpm 11's allowBuilds allowlist lives, and existing Dockerfiles typically only copy package.json and the lockfile into the build context. Without the workspace file, pnpm 11 sees every dep with a build script as "unaddressed" and fails (new v11 default behavior, not configurable per-stage).
Action — find and fix every install stage:
grep -nE '^COPY .*(package\.json|pnpm-lock\.yaml)' Dockerfile Dockerfile.* 2>/dev/null
For each COPY package.json pnpm-lock.yaml ./ (or similar) line in a stage that runs pnpm install / pnpm fetch, add pnpm-workspace.yaml:
- COPY package.json pnpm-lock.yaml ./
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
If you have separate deps and deps_prod (or build and prod) stages, both need the workspace file — the allowlist applies to --prod installs too. Also bump any corepack prepare pnpm@<old> lines to [email protected] so the pin matches the packageManager field (the field overrides the pin at install time, but a stale pin is confusing in review).
If the existing Dockerfile is broadly fine after that two-line patch, stop here — don't rewrite it from one of the patterns below. Only use the patterns when there's no existing Dockerfile or it needs a structural rewrite.
Pattern menu for new Dockerfiles:
--mount=type=cache requires BuildKit (default on Docker 23+). Declare it explicitly with # syntax=docker/dockerfile:1.7. Prefer node:22-slim over node:22-alpine unless you specifically need musl — Alpine + node-gyp + native deps adds friction.
Pattern 1 — single-stage (dev images, simple apps):
# syntax=docker/dockerfile:1.7
FROM node:22-slim
ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
CMD ["pnpm", "start"]
Pattern 2 — multi-stage with pnpm fetch (production):
pnpm fetch populates the store from the lockfile alone, so the dependency layer rebuilds only when pnpm-lock.yaml changes — not when source changes.
# syntax=docker/dockerfile:1.7
FROM node:22-slim AS base
ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable
WORKDIR /app
FROM base AS deps
COPY pnpm-lock.yaml pnpm-workspace.yaml* ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm fetch --prod
FROM base AS build
COPY . .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile --offline && \
pnpm run build
FROM node:22-slim AS runtime
ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
RUN corepack enable
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json
USER node
CMD ["node", "dist/index.js"]
Pattern 3 — monorepo deploy with pnpm deploy:
Extracts a self-contained, hoisted node_modules for one workspace package, with no workspace symlinks. Ready to copy into a slim runtime stage.
# syntax=docker/dockerfile:1.7
FROM node:22-slim AS base
ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
RUN corepack enable
WORKDIR /repo
FROM base AS build
COPY . .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile
RUN pnpm --filter=@my-org/api build
RUN pnpm deploy --filter=@my-org/api --prod /out
FROM node:22-slim AS runtime
WORKDIR /app
COPY --from=build /out .
USER node
CMD ["node", "dist/index.js"]
8l. docker-compose
The named pnpm-store volume persists the store across docker compose up cycles so local dev installs stay fast. Use docker compose (the v2 plugin), not legacy docker-compose.
services:
app:
image: node:22
working_dir: /app
environment:
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
PNPM_HOME: /pnpm
PATH: /pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- .:/app
- node_modules:/app/node_modules
- pnpm-store:/pnpm/store
command: sh -c "corepack enable && pnpm install && pnpm run dev"
volumes:
node_modules:
pnpm-store:
8m. .dockerignore
The lockfile and workspace config must stay in the build context. If a .dockerignore exists, read it and verify none of these patterns are excluded — bare * rules and broad globs are the usual offenders:
grep -E '(^|/)(pnpm-workspace\.yaml|pnpm-lock\.yaml|package\.json|\*\.yaml|\*\.json|\*$)' .dockerignore 2>/dev/null
If any of those files would be excluded, add explicit un-ignore rules (!pnpm-workspace.yaml, etc.) or narrow the offending glob. The Dockerfile patch in 8k is useless if .dockerignore strips the file out of the build context before COPY ever sees it.
Reference .dockerignore for a new project — exclude outputs and local-only files, keep dep manifests:
# Always exclude
node_modules
.git
.pnpm-store
dist
build
coverage
.next
.turbo
.cache
# Local-only files
.env
.env.local
.env.*.local
*.log
# Don't exclude — these MUST be in the build context:
# package.json
# pnpm-lock.yaml
# pnpm-workspace.yaml
If .npmrc holds an auth token, don't copy it into the image. Mount as a BuildKit secret:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
--mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile
Build with docker build --secret id=npmrc,src=$HOME/.npmrc ..
8n. devcontainer.json
{
"name": "App",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "22" }
},
"postCreateCommand": "corepack enable && corepack prepare pnpm@11 --activate && pnpm install",
"containerEnv": {
"COREPACK_ENABLE_DOWNLOAD_PROMPT": "0"
},
"mounts": [
"source=pnpm-store-${devcontainerId},target=/home/node/.local/share/pnpm/store,type=volume"
]
}
8o. Cross-cutting updates regardless of provider
.nvmrc/.tool-versions— bump to Node 22.engines.nodeinpackage.json—">=22".- CI env vars — rename every
npm_config_*topnpm_config_*. - Scripts that call
npx— preferpnpm dlx.dlxhonorsallowBuildsandstrictDepBuilds, so a dlx command needing build scripts will fail unless allowlisted. Makefile/justfiletargets callingnpm/yarn— switch topnpm.
8p. Sanity check after pushing
The first post-migration CI run should show all of:
- Node 22.x or newer.
- pnpm 11.x via corepack or
pnpm/action-setup. pnpm install --frozen-lockfilesucceeding with noERR_PNPM_IGNORED_BUILDS.pnpm audit signaturesrunning and either passing or returning a known list.- Meaningful cache hit on the second run.
If any are missing, re-run the detection grep — the pipeline got partially upgraded.
Step 9 — Summarize and stop
Summarize the changes for the user — files touched, packages added to allowBuilds, any findings from pnpm audit signatures, and any Dockerfile / CI edits. Do not run git commit, git push, or gh pr yourself. Let the user decide how to commit (their own workflow, a separate agent, or by hand).
Gotchas
pnpm install -gwith no arguments is removed. Per-global-package isolation now — eachpnpm add -g <pkg>gets its own dir underPNPM_HOME/bin.pnpm serveris gone. If the project's tooling used it, find an alternative.- Removed commands:
access,bugs,edit,issues,owner,prefix,profile,pkg,repo,set-script,team,token. Most have native equivalents or are no longer needed. useNodeVersion/executionEnv.nodeVersionremoved. UsedevEngines.runtimeor external version managers (nvm/mise/asdf).managePackageManagerVersions,packageManagerStrict,packageManagerStrictVersionall replaced bypmOnFail.- Git-hosted deps now get integrity pinning. The first install after migration writes
gitHosted: trueentries — subsequent installs detect tampering. Don't be alarmed by the lockfile diff. minimumReleaseAgeand CI cache: if CI restores a lockfile from cache that was resolved before v11's policy, pnpm re-validates and may refuse to install. This is intended.
Quick reference: file diff after a typical migration
package.json—packageManagerset,engines.nodebumped,pnpmfield removed..npmrc— trimmed to auth/registry lines only (or deleted if it only held pnpm settings).pnpm-workspace.yaml— created or expanded with hardening keys andallowBuilds.pnpm-lock.yaml— regenerated at lockfile v9 (no change if already on pnpm 10).package-lock.json/yarn.lock— deleted..nvmrc/.tool-versions— Node 22.- CI files — package manager and Node version updated.