Skip to content

vendor and vendor --check run from an npm workspace member directory exit 0 without looking at the root lock: the eject says "nothing to vendor" and the check passes over a tampered artifact #1921

Open

Description

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

Summary

In an npm workspace, the root package-lock.json installs every member, so a member directory (packages/a) has no lock or .socket/ of its own. When socket-patch vendor or socket-patch vendor --check runs from that member directory (or with --cwd packages/a), it looks only at the member directory, finds nothing, and exits 0:

  • vendor (the hosted → vendored eject) prints No manifest found, nothing to vendor. (--json: "status": "noManifest"), exit 0, while the root lock holds hosted pins. Nothing is ejected.
  • vendor --check prints nothing (--json: "status": "success", "verified": 0), exit 0, even when the root's committed .socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz is tampered. The same check from the root exits 1 with vendor_sha256_mismatch.

Hosted scan from the same directory refuses with redirect_workspace_lockfile_elsewhere and names the workspace root. Vendored scan / get refuse too (#1904, being fixed in #1913). The eject and the check have no such guard.

Impact

vendor --check is the read-only audit meant for CI. A CI step or pre-commit hook that runs it from a member package directory (common in monorepos, where per-package jobs cd into the package) always passes, whatever state the root's vendored artifacts and wiring are in. The eject, run the same way, reports a calm success and leaves the project on hosted patches when the user asked to vendor them. Neither run writes anything, but both report success for work they never did.

Repro (Linux, Node 22, npm 8.19.4 / 10.9.9 / 12.2.1)

mkdir -p ws/packages/a && cd ws
echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
npm install
socket-patch scan --mode hosted --yes            # root lock now pins the hosted left-pad
(cd packages/a && socket-patch vendor --yes; echo "exit=$?")
#   No manifest found, nothing to vendor.
#   exit=0                                         <-- root lock still hosted
socket-patch vendor --yes                         # from the root: "Vendored 1 package."
printf x >> .socket/vendor/npm/*/left-pad-1.3.0.tgz   # tamper the committed artifact
socket-patch vendor --check; echo "exit=$?"       # root: vendor_sha256_mismatch, exit=1
(cd packages/a && socket-patch vendor --check; echo "exit=$?")
#   (no output)
#   exit=0                                         <-- same project, same tamper

left-pad@1.3.0 stands for any package with an available patch. I used a local mock of the patch API (--api-url, --org, --patch-server-url all pointing at it) serving a free patch for pkg:npm/left-pad@1.3.0. The member-dir behaviour happens before any patch-specific work. socket-patch vendor --check --cwd packages/a from the root behaves the same as running it from the member.

Expected vs actual

  • Expected: docs/ecosystems.md says a hosted or vendored run from a workspace member "is refused (redirect_workspace_lockfile_elsewhere, vendor_lockfile_missing) and names the root to run from" (vlt notes; hosted scan does the same for npm). vendor and vendor --check from a member should refuse the same way, exit non-zero, and name the workspace root. At the least, vendor --check must not report success when the governing root's ledger fails the check.
  • Actual: both exit 0. vendor says "nothing to vendor"; vendor --check verifies zero entries and says nothing.

vex from the member also misses the root's pins, but it fails closed (exit 1, "no hosted or vendored patch references were found … nothing to attest") with no pointer to the root. That's a diagnostics gap only.

OS × version

OS npm lockfileVersion main cbbbdb9 v5.0.1 PR #1913 head
Linux 8.19.4 2 reproduces — —
Linux 10.9.9 3 reproduces reproduces reproduces
Linux 12.2.1 3 reproduces — —

macOS and Windows weren't probed (probe branches are blocked for this routine). The code path doesn't depend on the OS. This isn't a regression: v5.0.1 behaves the same.

Suspect code

  • crates/socket-patch-cli/src/commands/vendor.rs:1015: the eject reads hosted pins with hosted_inventory(&args.common, &args.common.cwd), cwd only. With no pins and no manifest it falls through to the noManifest exit-0 arm (vendor.rs:1054-1075). It never calls hosted::governing_root::refusal, which hosted scan uses (commands/scan/hosted.rs:991).
  • crates/socket-patch-cli/src/commands/vendor.rs:1253 (run_check): loads the ledger from args.common.project_root(). An absent ledger is an empty one, so the check passes with nothing verified, and nothing asks whether a workspace root governs this directory.
  • PR Fix vendored member refusal not naming root (#1904) #1913 adds the root-naming text only to the vendored lock-flavor detection (vendor/npm_flavor.rs), which neither path reaches. Yarn and Bun members probably behave the same, since the guard is missing before any per-manager code runs. I only tested npm.

Activity

  1. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (npm). Related to #1904 (vendored scan/get member-dir refusal, being fixed in #1913), but a separate code path: commands/vendor.rs returns the calm noManifest exit 0 (L2379) before any governing-root lookup, and vendor --check verifies the member dir's empty ledger. The fix should reuse the governing-root resolution #1913 extends in hosted/governing_root.rs, so it is best taken after #1913 lands. Not a duplicate.


    Generated by Claude Code

  2. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: vendor, vendor --revert and vendor --check never ask whether an ancestor workspace root governs --cwd). Branch: agent/fix-vendor-member-governing-root. Claim-ID: 2026-11-01T10:21:05Z-af9379


    Generated by Claude Code

  3. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Fix in progress: #1928


    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