Skip to content

Vendored scan/get run from an npm workspace member directory refuses with "no package-lock.json … run your package manager's install first", a remedy that loops; hosted names the workspace root #1904

Closed

Description

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

Summary

When socket-patch scan --mode vendored or get <uuid> --mode vendored runs from an npm workspace member directory (packages/a) that has no lock of its own, it refuses with vendor_lockfile_missing. That's correct: npm installs the member from the root package-lock.json. But the refusal says:

no package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock, bun.lockb, or vlt-lock.json in the project root — vendoring rewires the lockfile, so one must exist (run your package manager's install first)

Following that advice doesn't help. Running npm install in the member updates the root lock, and the next vendored run prints the same refusal. The message never says the directory is a workspace member or which directory to run from.

The other refusals for the same layout do name the root:

So the common case (a member with no lock) gets the least helpful message. docs/ecosystems.md (vlt workspace members) describes these refusals as redirect_workspace_lockfile_elsewhere / vendor_lockfile_missing "and names the root to run from".

Impact

The run fails closed (exit 1, nothing is written), so this is a diagnostics bug. A user who runs socket-patch from a member directory, which is common in monorepos, gets advice that loops and no pointer to the workspace root. The --json output has the same text under errorCode: vendor_lockfile_missing, so tooling can't tell this case apart from a project that really has no lock.

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

mkdir -p ws/packages/a && cd ws
echo '{"name":"app","version":"1.0.0","private":true,"workspaces":["packages/*"],"dependencies":{"plainpkg":"2.0.0"}}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"plainpkg":"1.0.0"}}' > packages/a/package.json
npm install            # root package-lock.json; plainpkg@1.0.0 lands in packages/a/node_modules
cd packages/a
socket-patch scan --mode vendored        # exit 1: "no package-lock.json … in the project root … run your package manager's install first"
npm install                              # follow the remedy: updates ../../package-lock.json
socket-patch scan --mode vendored        # same refusal again
socket-patch scan                        # hosted: redirect_workspace_lockfile_elsewhere, names the workspace root

plainpkg@1.0.0 stands for any package with an available patch. I used a local mock of the patch API and registry (SOCKET_PROXY_URL, SOCKET_PATCH_SERVER_URL, npm_config_registry). The refusal fires before any patch-specific work, so any patched package shows it.

Expected vs actual

  • Expected: the vendored refusal names the workspace root, as hosted and the stray-lock vendored refusal do. For example: ". is a workspace member; npm installs it from <root>/package-lock.json; vendor from <root> (the workspace root)". The code can stay vendor_lockfile_missing.
  • Actual: a generic "no lockfile, run install first" message, whose remedy loops.

OS × version

OS npm main cbbbdb9 v5.0.1
Linux 8.19.4 reproduces —
Linux 10.9.4 reproduces reproduces
Linux 12.2.1 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-core/src/vendor/npm_flavor.rs:250-262: step 4 ("Nothing recognizable") of the lock-flavor detection always returns the generic message. It doesn't consult hosted::governing_root (the package.json workspaces ancestor check that hosted's refusal uses).
  • crates/socket-patch-core/src/vendor/npm_flavor.rs:403-424 (member_stray_lock_refusal) builds the root-naming detail, but only when the member has a stray lock.
  • The same step-4 path serves yarn / Bun members, so those are probably affected too. I only tested npm.

Activity

  1. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (npm-family). Confirmed by code on main cbbbdb9: step 4 of the lock-flavor detection in vendor/npm_flavor.rs returns the generic vendor_lockfile_missing text without the hosted::governing_root ancestor check that hosted's refusal and member_stray_lock_refusal use. No duplicate or open PR found. This doesn't share a root cause with #1900 (that one is about how workspaces is parsed, not about which refusal is shown).


    Generated by Claude Code

  2. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: vendored lock-flavor detection's no-lock step never checks for a governing workspace root). Branch: agent/fix-vendored-member-lockfile-refusal. Claim-ID: 2026-11-01T04:20:58Z-36493f


    Generated by Claude Code

  3. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Fix in progress: #1913


    Generated by Claude Code

  4. added a commit that references this issue on Nov 1, 2026
    9106f37
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions