Skip to content

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

Closed

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: true there would not take effect in this environment, and nothing was written", which is wrong, and
  • leaves a lock that the next pnpm install --frozen-lockfile rejects 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 installed

Run 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: true unless it is "explicitly disabled ... for pnpm >=11, by a pnpm_config_trust_lockfile environment variable the scan runs with (pnpm reads it ahead of the file ...)". An empty, null or undefined value 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_bool for 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), via found.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

  1. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (pnpm). Confirmed a regression from #1533: PnpmSetting::as_bool (utils/pnpm_workspace.rs) uses the .npmrc falsy set ("", null, undefined) for the pnpm 11+ pnpm_config_trust_lockfile env override, which pnpm itself reads as false only for the literal false. hosted/engine.rs pnpm_trust_env_override and scan/hosted.rs trust_env_off share the predicate. Not a duplicate of #1514. No open PR covers it yet.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: pnpm 11+ trust_lockfile env override parsed with the .npmrc falsy set instead of pnpm's literal-false rule). Branch: agent/fix-pnpm-trust-env-empty. Claim-ID: 2026-10-15T12:21:27Z-b8e16c


    Generated by Claude Code

  3. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1575


    Generated by Claude Code

  4. added 2 commits that reference this issue on Oct 15, 2026
    935d3be
    9b470d4
  5. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] pnpm bug-hunt run 44: I proved the workspace-member half end to end (scan/hosted.rs trust_env_off) and checked it against draft PR #1575.

    Shape: a pnpm workspace with sharedWorkspaceLockfile: false in the root pnpm-workspace.yaml. Member packages/a has its own pnpm-lock.yaml with left-pad@1.3.0. I ran scan --mode hosted --cwd packages/a against a local patch-API mock.

    pnpm env main e76cb9b scan fresh pnpm install --frozen-lockfile in the member (same env) PR #1575 (9b470d4) scan
    11.28.5 unset refuses redirect_pnpm_settings_elsewhere (correct) n/a refuses (correct)
    11.28.5 pnpm_config_trust_lockfile= exit 0. Rewrites packages/a/pnpm-lock.yaml, writes no key fails ERR_PNPM_TARBALL_URL_MISMATCH (2/2 runs) refuses redirect_pnpm_settings_elsewhere (fixed); =undefined also refuses
    12.11.2 pnpm_config_trust_lockfile= same: lock rewritten, no key fails ERR_PNPM_TARBALL_URL_MISMATCH (2/2 runs) refuses (fixed); =undefined also refuses
    12.11.2 pnpm_config_trust_lockfile=false n/a n/a exit 0, rewrites with the #1514 warning (unchanged, correct)

    Control: adding trustLockfile: true by hand to the root pnpm-workspace.yaml makes the same frozen install pass with the empty variable on both 11 and 12, and the patched marker lands. pnpm config get trustLockfile prints undefined there with both an empty value and undefined. 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_off goes through the same as_bool predicate. A regression test for the member refusal with an empty variable might be worth adding there.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions