[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
pnpm's gitBranchLockfile setting (git-branch-lockfile=true in .npmrc on pnpm ≤10, gitBranchLockfile: true in pnpm-workspace.yaml) makes pnpm read and write pnpm-lock.<branch>.yaml instead of pnpm-lock.yaml. Hosted mode only looks at files named exactly pnpm-lock.yaml / shrinkwrap.yaml. That causes two failures:
- Both locks present (the usual case:
pnpm-lock.yaml committed on main, and a branch lock written once the feature branch changes deps): scan --mode hosted pins the stale pnpm-lock.yaml and reports status: success, redirected: 1. pnpm installs from pnpm-lock.feature.yaml, which still points at the registry, so a fresh pnpm install --frozen-lockfile silently installs the unpatched upstream bytes. vex honestly declines (not_applied), so it's the only signal.
- Only a branch lock present: the scan exits 0 with
redirected: 0 and the warning redirect_pnpm_no_lockfile, whose remedy ("run pnpm install to generate one") cannot work, because pnpm will just rewrite the branch lock.
Impact
On a feature branch with this setting, the user is told the patch is pinned (and the pin lands in a file they will commit), but CI and fresh checkouts on that branch install the vulnerable package. When the branch merges, pnpm's mergeGitBranchLockfiles flow can carry the unpinned branch entry over the pinned main one.
Repro (Linux, pnpm 12.8.1; local mock of the patch API serving a patched is-number@7.0.0 tarball)
mkdir app && cd app && git init -q -b main
echo '{"name":"app","version":"1.0.0","dependencies":{"is-number":"7.0.0"}}' > package.json
printf "packages:\n - '.'\n" > pnpm-workspace.yaml; echo node_modules > .gitignore
pnpm install && git add -A && git commit -qm init && git checkout -qb feature
printf "gitBranchLockfile: true\n" >> pnpm-workspace.yaml # pnpm 10: echo git-branch-lockfile=true > .npmrc
pnpm add is-odd@3.0.1 # writes pnpm-lock.feature.yaml; pnpm-lock.yaml stays as on main
socket-patch scan --mode hosted --json --yes --api-url $MOCK --org test-org --api-token fake
# -> status success, redirected 1, rewrittenFiles [pnpm-lock.yaml, pnpm-workspace.yaml]
grep -c $MOCK pnpm-lock.yaml pnpm-lock.feature.yaml # 1 / 0
# fresh checkout, empty store:
rm -rf node_modules && pnpm install --frozen-lockfile --store-dir $(mktemp -d)
head -1 node_modules/is-number/index.js # "/*!" (upstream), not the patch marker
socket-patch vex --output v.json # "omitting pkg:npm/is-number@7.0.0 ... (not_applied)"
Only-branch-lock variant: git init -b feature, enable the setting before the first pnpm install. Hosted scan then gives redirected 0 plus redirect_pnpm_no_lockfile.
Expected vs actual
- Expected: CLI_CONTRACT and docs/ecosystems.md say hosted mode pins the lock the package manager actually installs from, or refuses loudly when it can't. So hosted mode should either rewrite
pnpm-lock.<current-branch>.yaml (and leave the stale lock alone, or pin both), or refuse with a dedicated warning code that names the setting.
- Actual: it pins a file pnpm ignores on this branch, and reports success. With only the branch lock, it gives a misleading "no lockfile" remedy.
Matrix (Linux; each cell run twice)
| pnpm |
branch lock written |
both-locks case |
only-branch-lock case |
| 9.15.9 |
no (this fixture's .npmrc setting produced no branch lock) |
n/a: pinned pnpm-lock.yaml is the one used, patched |
n/a |
| 10.34.5 |
yes |
fail: success, fresh frozen install unpatched |
not run |
| 11.28.3 |
yes |
fail |
not run |
| 12.8.1 |
yes |
fail |
fail (misleading redirect_pnpm_no_lockfile) |
macOS and Windows weren't tested (no probe branch this run); the logic is filename-based, so they're likely the same. Vendored mode with this setting wasn't tested.
Suspect code
crates/socket-patch-core/src/formats/pnpm/hosted.rs:180 – lock_keys matches only pnpm-lock.yaml / shrinkwrap.yaml.
crates/socket-patch-core/src/formats/registry.rs:72 – the hosted read set lists only pnpm-lock.yaml.
crates/socket-patch-core/src/patch/redirect/mod.rs:806 – redirect_pnpm_no_lockfile fires without considering pnpm-lock.*.yaml.
Tested on main 61cfb9b (CLI 4.0.0).
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
pnpm's
gitBranchLockfilesetting (git-branch-lockfile=truein.npmrcon pnpm ≤10,gitBranchLockfile: trueinpnpm-workspace.yaml) makes pnpm read and writepnpm-lock.<branch>.yamlinstead ofpnpm-lock.yaml. Hosted mode only looks at files named exactlypnpm-lock.yaml/shrinkwrap.yaml. That causes two failures:pnpm-lock.yamlcommitted on main, and a branch lock written once the feature branch changes deps):scan --mode hostedpins the stalepnpm-lock.yamland reportsstatus: success, redirected: 1. pnpm installs frompnpm-lock.feature.yaml, which still points at the registry, so a freshpnpm install --frozen-lockfilesilently installs the unpatched upstream bytes.vexhonestly declines (not_applied), so it's the only signal.redirected: 0and the warningredirect_pnpm_no_lockfile, whose remedy ("runpnpm installto generate one") cannot work, because pnpm will just rewrite the branch lock.Impact
On a feature branch with this setting, the user is told the patch is pinned (and the pin lands in a file they will commit), but CI and fresh checkouts on that branch install the vulnerable package. When the branch merges, pnpm's
mergeGitBranchLockfilesflow can carry the unpinned branch entry over the pinned main one.Repro (Linux, pnpm 12.8.1; local mock of the patch API serving a patched
is-number@7.0.0tarball)Only-branch-lock variant:
git init -b feature, enable the setting before the firstpnpm install. Hosted scan then givesredirected 0plusredirect_pnpm_no_lockfile.Expected vs actual
pnpm-lock.<current-branch>.yaml(and leave the stale lock alone, or pin both), or refuse with a dedicated warning code that names the setting.Matrix (Linux; each cell run twice)
.npmrcsetting produced no branch lock)pnpm-lock.yamlis the one used, patchedredirect_pnpm_no_lockfile)macOS and Windows weren't tested (no probe branch this run); the logic is filename-based, so they're likely the same. Vendored mode with this setting wasn't tested.
Suspect code
crates/socket-patch-core/src/formats/pnpm/hosted.rs:180–lock_keysmatches onlypnpm-lock.yaml/shrinkwrap.yaml.crates/socket-patch-core/src/formats/registry.rs:72– the hosted read set lists onlypnpm-lock.yaml.crates/socket-patch-core/src/patch/redirect/mod.rs:806–redirect_pnpm_no_lockfilefires without consideringpnpm-lock.*.yaml.Tested on main
61cfb9b(CLI 4.0.0).