Skip to content

After npm install <pkg>@<other version> moves a vendored npm package off its patched version, scan --prune, vendor --revert, remove and rollback all drift-keep it, so vendor --check stays red and every remedy it names loops #1155

Description

[agent] Found by the scheduled npm bug-hunt routine (ledger #302).

Summary

Vendor a package, then upgrade or downgrade it with npm (npm install ms@2.1.3 over a vendored ms@2.1.2). The lock entry node_modules/ms now resolves the new version from the registry, and nothing references .socket/vendor/npm/<uuid>/ any more. After that:

  • vendor --check exits 1: dependency removed: no lockfile resolves pkg:npm/ms@2.1.2 any more (it was upgraded or uninstalled) … run socket-patch scan --mode vendored --prune to revert the vendored entry.
  • scan --mode vendored --prune exits 0 and reverts nothing. --json shows gc.keptVendoredEntries: ["pkg:npm/ms@2.1.2"]. The human output just says "No patches available" (that's the Human scan --mode vendored --prune silently skips the vendored GC when no remaining package has a patch, so an npm uninstalled vendored entry is never reverted (exit 0), while --json reverts it and vendor --check keeps pointing at that same command #1127 gap; with another patched package still installed it prints the "kept 1 drifted vendored entry … undo the drift" line instead).
  • vendor --revert exits 0: lock entry node_modules/ms was re-resolved since vendoring (now resolved to https://registry.npmjs.org/ms/-/ms-2.1.3.tgz); left alone and Kept 1 drifted package … undo the drift and re-run vendor --revert to finish.
  • remove pkg:npm/ms@2.1.2 exits 1: 1 matching entry was drift-kept …; re-run scan --mode vendored to normalize, then remove again. Re-running the scan changes nothing.
  • rollback pkg:npm/ms@2.1.2 exits 1: Kept vendored state … lockfile wiring drifted.
  • vendor --check is still 1.

"Undo the drift" would mean downgrading back to the vulnerable version. The only way out is deleting .socket/vendor/npm/<uuid>/ and editing .socket/vendor/state.json by hand.

The Bun routine found this first and handed it to the npm ledger. Bun's text and binary locks behave the same. #1132 (Bun bun.lockb removal), #1140 (uv) and #1142 (Pipenv) are the per-backend removal variants. On npm, a plain npm uninstall is reverted correctly (the vendor_lock_entry_removed arm from #665/#689). Only the re-resolved case loops.

Impact

Any project that upgrades a vendored dependency (the normal way to drop a patch once upstream ships a fix, and something Dependabot or Renovate does by itself) is left with a committed orphan tarball and a ledger entry. vendor --check then fails CI permanently, and none of the remedies socket-patch suggests can clear it. Nothing is installed unpatched (the new version installs from the registry), so this is a stuck-state / CI-gate bug, not a silent unpatching.

Repro (Linux, real npm, local mock of the public patch proxy serving a free patch for ms@2.1.2)

export SOCKET_PROXY_URL=http://127.0.0.1:18732 SOCKET_API_URL=http://127.0.0.1:18732 NO_PROXY=127.0.0.1
git init -q proj && cd proj
echo '{"name":"proj","version":"1.0.0","dependencies":{"ms":"2.1.2"}}' > package.json
npm install && git add -A && git commit -qm init
socket-patch scan --mode vendored && npm install && git add -A && git commit -qm vendored
socket-patch vendor --check; echo $?          # 0: committed artifact and wiring verified
npm install ms@2.1.3                          # lock: node_modules/ms -> registry ms-2.1.3.tgz
socket-patch vendor --check; echo $?          # 1: "dependency removed … upgraded or uninstalled … run scan --mode vendored --prune"
socket-patch scan --mode vendored --prune --json | jq '.gc | {revertedVendoredEntries, keptVendoredEntries}'
                                              # {"revertedVendoredEntries": [], "keptVendoredEntries": ["pkg:npm/ms@2.1.2"]}
socket-patch vendor --revert; echo $?         # 0, "Kept 1 drifted package … undo the drift"
socket-patch remove pkg:npm/ms@2.1.2; echo $? # 1, "re-run scan --mode vendored to normalize, then remove again"
socket-patch rollback pkg:npm/ms@2.1.2; echo $? # 1, "lockfile wiring drifted"
socket-patch vendor --check; echo $?          # still 1
ls .socket/vendor/npm/                        # 22222222-… still there

A downgrade (left-pad 1.3.0 → npm install left-pad@1.2.0) behaves the same.

Expected vs actual

  • Expected: CLI_CONTRACT.md:137 says scan --prune leg (b) reverts "EVERY ledger entry whose dependency is no longer in the lockfile graph". pkg:npm/ms@2.1.2 is no longer in the graph. vendor --check (and the doc comment on unwired_check_failure, crates/socket-patch-cli/src/commands/vendor.rs:396) names "upgraded or uninstalled" and scan --prune as the fix. So prune, and also vendor --revert / remove / rollback, should drop the ledger entry and the artifact, leaving the user's upgraded lock entry alone.
  • Actual: the npm revert classifies the re-resolved entry as vendor_lock_entry_drifted, so outcome.drift_skipped() keeps the artifact and ledger entry. Every command reports "kept", and vendor --check keeps sending you to the same prune.

Matrix (main c4235a2, Linux, Node 22.22 / Node 24.21 for npm 12)

OS npm lock upgrade (ms 2.1.2→2.1.3) downgrade (left-pad 1.3.0→1.2.0) npm uninstall (control)
Linux 8.19.4 v2 loops – reverted (verified 2026-10-07, ledger #302)
Linux 10.9.4 v3 loops (×2) loops reverted (--json prune, this run)
Linux 12.2.0 v3 loops – reverted (verified 2026-10-07, ledger #302)

(On npm 8 the pre-upgrade vendor --check is already red because of #879. That's unrelated.) macOS and Windows weren't probed: the code path is platform-independent lock JSON handling.

Not bisected: main's history is grafted, so commit bisects aren't possible. The "upgraded or uninstalled … --prune" check text came with #970/#1050.

Suspect code

  • crates/socket-patch-core/src/vendor/npm_lock.rs:1209-1231 (revert_one_record): when the live entry exists but its resolved no longer points into our uuid dir, it always warns vendor_lock_entry_drifted and returns. It doesn't check whether the live entry still names the vendored version. An entry whose version differs from rec.original/rec.new (or, more generally, one that Discovery::vendor_entry_in_use already calls Some(false)) is the same "dependency left the graph" case as the removed arm above it (lines 1189-1197), and should be handled like LOCK_ENTRY_REMOVED_CODE: nothing to restore, artifact releasable.
  • crates/socket-patch-cli/src/commands/vendor.rs run_vendor_gc (~4202) then lists the entry under kept, disagreeing with the in-use verdict that vendor --check uses.

Activity

  1. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (npm). This is the re-resolved sibling of the removal cases in #1132 (bun.lockb), #1140 (uv) and #1142 (Pipenv). Those are being fixed in the open PR #1147, which maps a lock entry that left the graph to vendor_lock_entry_removed instead of drift. #1147 doesn't cover npm, and doesn't cover a live entry whose version changed: npm_lock.rs revert_one_record still warns vendor_lock_entry_drifted whenever resolved leaves the uuid dir, without checking the version. So this needs its own change, ideally reusing the in-use verdict that vendor --check already uses (Discovery::vendor_entry_in_use) so the npm and Bun reverts agree with the check. No open PR addresses it yet; it is eligible to be claimed.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the npm-family vendored reverts treat a lock entry that now resolves a different version as drift instead of "the dependency left the graph"). Branch: agent/fix-vendored-reresolved-version-drift. Claim-ID: 2026-10-08T22:21:20Z-6f5c80


    Generated by Claude Code

  3. added a commit that references this issue on Oct 8, 2026
    a9bcc0b
  4. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1187 (npm and Bun text-lock reverts; bun.lockb is left for a follow-up after #1147 lands).


    Generated by Claude Code

  5. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-check from the npm bug-hunt routine (ledger #302): still reproduces on main 16106b1 (Linux, npm 10.9.4), and one more shape for #1187 to cover.

    When the new version also has a patch (mock offers ms@2.1.2 and ms@2.1.3): vendored ms@2.1.2, then npm install ms@2.1.3, then scan --mode vendored --prune --yes. The scan vendors and wires ms@2.1.3 (npm ci installs the patched 2.1.3), but it still prints GC: kept 1 drifted vendored entry for 2.1.2. Its artifact .socket/vendor/npm/<2.1.2 uuid>/ and its ledger entry stay, and vendor --check keeps exiting 1 with pkg:npm/ms@2.1.2: dependency removed … run socket-patch scan --mode vendored --prune. So upgrading to a version that has its own patch, which is the normal upgrade path, also loops.


    Generated by Claude Code

  6. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Bun re-check from the Bun bug-hunt routine (ledger #306), main 793edd4, Linux, real Bun, local mock of the patch API (minimist@1.2.2 + left-pad@1.3.0 vendored, then bun add minimist@1.2.8, then scan --mode vendored --prune --yes):

    Bun lock after the upgrade: prune vendor --check
    1.4.2 bun.lockb GC: reverted 1 vendored entry, minimist uuid dir removed exit 0
    1.4.2 text bun.lock (v2) GC: kept 1 drifted vendored entry; vendor --revert also says Kept 1 drifted package (exit 0) exit 1 ("dependency removed … run scan --mode vendored --prune")

    So after #1147 the binary bun.lockb backend already handles the re-resolved case (its revert now reads an entry that left the graph as removed), and the "bun.lockb follow-up" that #1187 mentions may not be needed. Bun's text-lock backend still loops, as #1187 expects.


    Generated by Claude Code

  7. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] pnpm re-check from the pnpm bug-hunt routine (ledger #303). The pnpm vendored backends have the same loop, and draft PR #1187 doesn't touch them (it changes npm_lock.rs / bun_lock.rs only).

    Main f3c6313, Linux, Node 22, real pnpm installs, local mock of the patch API serving left-pad@1.3.0. Steps: vendor left-pad@1.3.0 (scan --mode vendored), pnpm install, then pnpm add -E left-pad@1.2.0 (-w on the 9.x workspace scaffold), then rm -rf node_modules && pnpm install so #1197's orphaned .pnpm dir can't interfere, then each command in turn:

    pnpm lock vendor --check scan --mode vendored --prune vendor --revert remove pkg:npm/left-pad@1.3.0 vendor --check after all
    8.15.9 6.0 (legacy backend) 1 0, gc.keptVendoredEntries 0, kept 1 (partialFailure) 1
    9.15.9 9.0 1 0, kept 0, kept 1 1
    10.34.6 9.0 1 0, kept 0, kept 1 1
    11.28.5 9.0 1 0, kept 0, kept 1 1
    12.10.1 9.0 1 0, kept (2×) 0, kept 1 1

    The ledger entry and .socket/vendor/npm/<uuid>/ stay in every row. A plain scan --mode vendored keeps warning vendor_ledger_entry_unwired ("run socket-patch scan --prune to revert it") on every run.

    vendor --revert on 12.10.1:

    vendor_lock_entry_removed | snapshots entry `left-pad@file:.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz` no longer exists; nothing to restore
    vendor_lock_entry_removed | packages entry `left-pad@file:.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz` no longer exists; nothing to restore
    vendor_lock_entry_drifted | importer dep `.|left-pad` was re-resolved since vendoring (1.2.0); left alone
    vendor_revert_kept        | lock entries drifted since vendoring; artifacts and ledger entry kept — undo the drift and re-run `vendor --revert` to finish
    

    The packages / snapshots entries are already read as removed. Only the importer dependency (.|left-pad now 1.2.0) counts as drift, and that alone keeps the entry. Same for the legacy backend's root dependency.

    One more side effect: the pruning scan still removes the dead left-pad@1.3.0 override from package.json, the lock's overrides: and pnpm-workspace.yaml (deleting the scaffold file), yet reports the entry as kept. The ledger's recorded wiring then no longer matches the files.

    Suspect code (the same "version changed means drift" check as npm_lock.rs revert_one_record):

    • crates/socket-patch-core/src/vendor/pnpm_lock.rs:3107 (importer dep) and :3261 (snapshot ref)
    • crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs:1069 (root dep) and :1236 (dep ref), for pnpm 7/8

    A fix for #1155 should cover these too, ideally through the same vendor_entry_in_use verdict. Not a regression I could bisect (4.0.0 doesn't speak the v5 mock contract). Not probed on macOS/Windows; the logic isn't path-dependent.


    Generated by Claude Code

  8. added
    v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.
    uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.
    compatibilityPublic CLI/JSON, saved state, upgrades, or package-manager compatibility.
    on Oct 9, 2026
  9. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 release blocker (P1). Normal npm dependency upgrades must not leave vendor --check permanently red with a cleanup command that cannot work. This is ordinary package maintenance, not a stress loop; PR #1187 is pending.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

  10. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] The fix PR #1187 is blocked on something outside it. Its own checks pass (build, clippy, unit tests and the other e2e suites). The only red jobs are hosted-e2e, e2e_safety_pnpm and the Bun native compat legs. They fail on every PR because production stopped publishing the free minimist@1.2.2 patch those suites are pinned to (#1293).

    What a human needs to do:

    • Either republish that patch, or repin the live-service suites to another free npm patch, following docs/testing/hosted-production-e2e.md § "If a required patch is withdrawn". The agent sandbox can't reach patches-api.socket.dev, so it can't pick a replacement itself.
    • Re-approve Fix vendored npm/Bun revert treating an upgrade as drift (#1155) #1187. The existing approval was given on 264b6ff, and four commits landed after it.

    Generated by Claude Code

  11. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] The main-wide canary failure that was blocking #1187 is fixed by #1302 (repinned to the republished patch 642d7f02-…). Once #1302 merges, #1187 needs a merge from main, a CI rerun, and a re-approval.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:claimedagent:needs-humanagent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentcompatibilityPublic CLI/JSON, saved state, upgrades, or package-manager compatibility.pm:npmnpmpriority:p1uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions