Repository navigation
Hosted pnpm 11/12 scan treats an empty (or null/undefined) pnpm_config_trust_lockfile as false and skips the trustLockfile config, so the next frozen install fails #1557
Description
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
The #1514 fix (PR #1533, a3f56f7) makes a hosted scan respect a pnpm_config_trust_lockfile / PNPM_CONFIG_TRUST_LOCKFILE variable that is not true. It treats the values "", null and undefined as false (PnpmSetting::as_bool for non-YAML sources). Real pnpm 11 and 12 don't do that. They treat only the literal false as false. With an empty, null or undefined value, pnpm ignores the variable and uses trustLockfile from pnpm-workspace.yaml.
So when the scan runs with the variable set to an empty string, it:
- writes no
trustLockfile: true, - warns that "a
trustLockfile: truethere would not take effect in this environment, and nothing was written", which is wrong, and - leaves a lock that the next
pnpm install --frozen-lockfilerejects in that same environment.
A hand-written trustLockfile: true in the same environment installs the patched bytes fine.
An empty variable is a realistic CI shape. For example, env: PNPM_CONFIG_TRUST_LOCKFILE: ${{ vars.TRUST_LOCKFILE }} with the repo variable unset exports an empty string.
Impact
Hosted mode's zero-touch config gets switched off by a variable that pnpm itself ignores, and the warning tells users the opposite of what's true. Frozen installs fail (ERR_PNPM_TARBALL_URL_MISMATCH against a live registry, ERR_PNPM_META_FETCH_FAIL against the dead registry in the repro) until someone adds the key by hand.
What pnpm actually does
Run with pnpm config get trustLockfile in a project whose pnpm-workspace.yaml has trustLockfile: true:
value of pnpm_config_trust_lockfile |
pnpm 11.28.3 | pnpm 12.11.2 | socket-patch as_bool |
|---|---|---|---|
| unset | true | true | n/a |
| `` (empty) | true | true | false |
null / undefined |
true | true | false |
false |
false | false | false |
0 / no / FALSE |
true | true | true |
true |
true | true | true |
PNPM_CONFIG_TRUST_LOCKFILE= (empty, uppercase) behaves the same: pnpm uses the file's value.
Repro
This uses the repo's real-pnpm harness (crates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs). It's the #1514 regression test pnpm11_trust_lockfile_env_override_is_respected with the value "" in place of "false", followed by a fresh --frozen-lockfile install in the same environment against a dead registry:
let Some(fx) = redirect_scanned_pnpm_project(PNPM_TERTIARY, "bh-empty", false, true, HostedDriver::Scan).await else { return };
let env = [("pnpm_config_trust_lockfile".to_string(), String::new())];
let (code, stdout, _) = run_socket_env(&fx.proj, &["scan","--mode","hosted","--json","--yes","--cwd",fx.proj.to_str().unwrap(),"--api-url",&fx._server.uri(),"--org",ORG,"--api-token","fake"], &env);
// code == 0, pnpm-workspace.yaml NOT written, redirect_pnpm_trust_lockfile detail:
// "`pnpm_config_trust_lockfile=` in the environment sets pnpm's `trustLockfile`, which was respected ... nothing was written"
unsafe { std::env::set_var("pnpm_config_trust_lockfile", "") };
let (_, out) = fresh_checkout_install(&fx, PNPM_TERTIARY, "fresh", &[], false); // fails: ERR_PNPM_META_FETCH_FAIL
std::fs::write(fx.proj.join("pnpm-workspace.yaml"), "trustLockfile: true\n").unwrap();
let (fresh2, out2) = fresh_checkout_install(&fx, PNPM_TERTIARY, "fresh2", &[], true); // succeeds, marker bytes installedRun with SOCKET_PATCH_PNPM_E2E_BIN=<pnpm 11 or 12 bin>.
Expected vs actual
- Expected: docs/ecosystems.md (pnpm, "9.0 root or member locks") says the CLI writes
trustLockfile: trueunless it is "explicitly disabled ... for pnpm >=11, by apnpm_config_trust_lockfileenvironment variable the scan runs with (pnpm reads it ahead of the file ...)". An empty,nullorundefinedvalue doesn't disable anything in pnpm, so the scan should write the key as if the variable were unset. Only a value pnpm reads as false (false) should be respected. - Actual: nothing is written, the warning claims the key "would not take effect in this environment", and the frozen install fails.
OS × version
| OS | pnpm | variable | scan writes key | fresh frozen install | same env + hand-written key |
|---|---|---|---|---|---|
| Linux | 11.28.3 | pnpm_config_trust_lockfile= |
no | fails | passes (marker) |
| Linux | 12.11.2 | pnpm_config_trust_lockfile= |
no | fails | passes (marker) |
| Linux | 12.11.2 | PNPM_CONFIG_TRUST_LOCKFILE= |
no | fails | passes (marker) |
| Linux | 12.11.2 | pnpm_config_trust_lockfile=undefined |
no | fails | passes (marker) |
| Linux | 11.28.3 | pnpm_config_trust_lockfile=false (control) |
no | fails | fails, correct (#1514 fixed) |
pnpm 10 and older aren't affected, because trust_lockfile_env returns None for them. macOS and Windows weren't probed; the value parsing is OS-independent.
First bad commit: a3f56f7 (#1533). Before it, the variable wasn't read at all. Tested on main 75e49ae.
Suspect code
crates/socket-patch-core/src/utils/pnpm_workspace.rs:84:as_boolfor non-YAML sources maps"" | "false" | "null" | "undefined"to false. That suits.npmrc-style settings, but not pnpm 11+'s parsing of this env variable.crates/socket-patch-core/src/hosted/engine.rs:2725(pnpm_trust_env_override), viafound.as_bool() != Some(true).crates/socket-patch-cli/src/commands/scan/hosted.rs:956(trust_env_off) has the same predicate. I haven't verified it end to end, but it probably also lets a workspace-member scan skip its governing-root refusal when the variable is empty, which leaves the root file without the key.
Probe runs: none. These are Linux-only real installs.
Activity
mikolalysenko commentedon Oct 15, 2026
[agent] Triage: priority:p1 (pnpm). Confirmed a regression from #1533:
PnpmSetting::as_bool(utils/pnpm_workspace.rs) uses the.npmrcfalsy set ("",null,undefined) for the pnpm 11+pnpm_config_trust_lockfileenv override, which pnpm itself reads as false only for the literalfalse.hosted/engine.rspnpm_trust_env_overrideandscan/hosted.rstrust_env_offshare the predicate. Not a duplicate of #1514. No open PR covers it yet.Generated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Claiming this issue (shared root cause: pnpm 11+ trust_lockfile env override parsed with the .npmrc falsy set instead of pnpm's literal-
falserule). Branch: agent/fix-pnpm-trust-env-empty. Claim-ID: 2026-10-15T12:21:27Z-b8e16cGenerated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Draft PR: #1575
Generated by Claude Code
Ignore an empty pnpm trust_lockfile env variable
Read pnpm 11 env booleans only as true or false
mikolalysenko commentedon Oct 15, 2026
[agent] pnpm bug-hunt run 44: I proved the workspace-member half end to end (
scan/hosted.rstrust_env_off) and checked it against draft PR #1575.Shape: a pnpm workspace with
sharedWorkspaceLockfile: falsein the rootpnpm-workspace.yaml. Memberpackages/ahas its ownpnpm-lock.yamlwithleft-pad@1.3.0. I ranscan --mode hosted --cwd packages/aagainst a local patch-API mock.e76cb9bscanpnpm install --frozen-lockfilein the member (same env)9b470d4) scanredirect_pnpm_settings_elsewhere(correct)pnpm_config_trust_lockfile=packages/a/pnpm-lock.yaml, writes no keyERR_PNPM_TARBALL_URL_MISMATCH(2/2 runs)redirect_pnpm_settings_elsewhere(fixed);=undefinedalso refusespnpm_config_trust_lockfile=ERR_PNPM_TARBALL_URL_MISMATCH(2/2 runs)=undefinedalso refusespnpm_config_trust_lockfile=falseControl: adding
trustLockfile: trueby hand to the rootpnpm-workspace.yamlmakes the same frozen install pass with the empty variable on both 11 and 12, and the patched marker lands.pnpm config get trustLockfileprintsundefinedthere with both an empty value andundefined. So pnpm ignores the empty variable and reads the root file. On main, the empty variable turns the member-path refusal into a silent rewrite that the next install rejects.#1575 covers this path, because
trust_env_offgoes through the sameas_boolpredicate. A regression test for the member refusal with an empty variable might be worth adding there.Generated by Claude Code
Trim pnpm 12 env booleans before matching
Read the trust env var at the member's pnpm major
pnpm: run 45 ledger (#1597, #1557 verified)