Skip to content

Hosted and vendored scans from a nested directory under a vlt.json root that is not a workspace member write its stray vlt-lock.json and exit 0, though vlt installs from the root there; VEX then attests not_affected (also vlt 1.3.8 pnpm-workspace.yaml dirs, contrary to #1574's premise) #1584

Closed

Description

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

Summary

vlt doesn't make a nested directory its own project unless the directory has its own vlt.json. Its project-root lookup (find("project") in vlt's config package, the same in 1.3.8 and 1.4.0) walks up from the cwd and stops at the nearest ancestor vlt.json. When there's none, it picks the outermost directory with a package.json / node_modules, stopping at .git or $HOME. So in repo/tools/x with no tools/x/vlt.json, vlt install and vlt ci operate on repo/ and never read tools/x/vlt-lock.json.

socket-patch refuses that layout only when the directory is a listed workspace member (#942, #1134, #1542, #1564). For any other nested directory, such as tools/x with no workspaces field listing it, a stray tools/x/vlt-lock.json is treated as that directory's own lock:

  • hosted pins it and exits 0 (applied);
  • vendored rewrites tools/x/package.json to file:.socket/vendor/... plus the stray lock, and exits 0;
  • vex then attests not_affected, including in vendored mode while the installed tools/x/node_modules/left-pad is pristine.

The same gap covers vlt 1.3.8 (and older) with a root pnpm-workspace.yaml. #1574 deliberately treats such a directory as its own project when the root lock has no member edges (governing_root.rs:872: "older vlt installs each directory the file lists as its own project"). That premise doesn't hold: vlt 1.3.8 vlt ci from packages/a installs the root project.

Impact

A scan reports success and VEX attests not_affected, but nothing vlt installs is patched, and a vendored run can leave a pristine copy on disk next to the attestation. A stray lock appears when a directory was once its own vlt project (it had a vlt.json, later removed or moved up when the repo adopted vlt at the root), when it's copied from elsewhere, or when it's left behind by tooling. #1134 / #1542 / #1564 accepted the same premise for listed members.

Repro (vlt 1.4.0 or 1.3.8, main 4cc50f4; also release 5.0.1)

Use the mock registry / patch API attached to #1494 (PORT=18555 node mock.mjs & and PORT=18556 node mock.mjs &), with the env from #1494.

mkdir -p repo/tools/x repo/.git && cd repo
CFG='{"config":{"registry":"http://127.0.0.1:18555/","registries":{"npm":"http://127.0.0.1:18555/"}}}'
echo '{"name":"root","version":"1.0.0","private":true,"dependencies":{"right-pad":"1.3.0"}}' > package.json
echo '{"name":"x","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > tools/x/package.json
# tools/x was once its own vlt project, then the repo moved vlt config to the root
(cd tools/x && echo "$CFG" > vlt.json && vlt install --allow-scripts :scripts && rm vlt.json)
echo "$CFG" > vlt.json && vlt install --allow-scripts :scripts
cd tools/x
vlt ci --allow-scripts :scripts         # installs repo/ (right-pad), never reads tools/x/vlt-lock.json
socket-patch scan --mode vendored --yes --json   # exit 0, status success, applied
vlt ci --allow-scripts :scripts         # exit 0, still the root project
node -e "console.log(require('left-pad'))"     # pristine (the old copy)
socket-patch vex -O vex.json            # exit 0, pkg:npm/left-pad@1.3.0 not_affected

With --mode hosted, the scan pins tools/x/vlt-lock.json (exit 0) and deletes the stale copy, vlt ci never brings anything back (require fails), and vex attests not_affected.

vlt 1.3.8 pnpm-workspace.yaml variant: the root has pnpm-workspace.yaml with packages: ["packages/*"] and no workspaces field. packages/a has a stray vlt-lock.json. The root lock was written by 1.3.8, so it has no member edges. Hosted and vendored scans from packages/a both exit 0 and write the member lock, vlt ci from packages/a installs the root, and vex attests not_affected. On vlt 1.4.0 the same layout is correctly refused after #1574.

Expected vs actual

OS × version

OS vlt layout hosted vendored control (own vlt.json in tools/x)
Linux 1.4.0 unlisted nested dir fail (2/2) fail (2/2) pass (patched after vlt ci)
Linux 1.3.8 unlisted nested dir fail (2/2) fail (2/2) pass
Linux 1.3.8 pnpm-workspace.yaml dir, 1.3.8 root lock fail fail —
Linux 1.4.0 pnpm-workspace.yaml member pass (refused, #1574) pass (refused) —

macOS / Windows: untested (probe branches are blocked in this routine). vlt's lookup is platform-independent JS.

First bad version: not a regression. Release 5.0.1 behaves identically (1.4.0, hosted and vendored).

Suspect code

  • crates/socket-patch-core/src/hosted/governing_root.rs:368 (member_stray_lock): for the vlt family it only consults vlt_lock_root (:442), which finds listed-member roots (vlt_workspace_root at :787, plus package.json workspaces). It has no case for "an ancestor vlt.json exists, so vlt's project root is there".
  • governing_root.rs:872-877 (vlt_lock_reads_pnpm_workspace): the "older vlt installs each directory the file lists as its own project" premise.

Activity

  1. mikolalysenko commented on Oct 15, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (vlt, npm family). Confirmed on main 4cc50f4 by reading the code: member_stray_lock (crates/socket-patch-core/src/hosted/governing_root.rs ~368) only treats a vlt lock as stray when vlt_lock_root finds a root that lists the directory as a workspace member; there is no case for an ancestor vlt.json (vlt's project root) above an unlisted directory, and vlt_lock_reads_pnpm_workspace (~872) keeps the older-vlt "each listed dir is its own project" premise. Not a duplicate: #1134 / #1542 / #1564 / #1574 cover listed members only. No open PR addresses it.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 16, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] vlt bug-hunt routine (ledger #307), more data on main 657fce2. A listed workspace member slips through as well when its workspaces glob has a .. segment.

    vlt normalizes .. in workspace globs, but utils/workspace_globs.rs matches components literally, apart from yarn berry's picomatch dialect. So for these vlt.json workspaces values, vlt records the member edge, but member_stray_lock doesn't see a member:

    workspaces vlt member edge run from vlt 1.2.0 vlt 1.3.8 vlt 1.4.0
    ["packages/../other/*"] (also with ./) workspace~other+o other/o — fail (vendored) fail (hosted 2/2, vendored 1/1)
    ["other/../packages/*"] workspace~packages+a packages/a fail (hosted) fail (vendored) fail (hosted)
    ["packages/x/../a"] (packages/x absent) workspace~packages+a packages/a fail (hosted)
    ["packages/*/../a"] workspace~packages+a packages/a fail (hosted)
    controls: packages/*, "packages/*" (string), {"g":"packages/*"}, packages/a/, packages//a, packages/./a, packages/.*, packages/{a,.x}, packages/!(a) member member pass (refused redirect_workspace_lockfile_elsewhere)

    The symptom matches this issue. Every failing cell exits 0 and rewrites the member's stray vlt-lock.json. The vendored cell also rewrites the member's package.json to file:.socket/vendor/..., after which vlt ci fails "out of sync" at both the member and the root. The copy on disk stays pristine, and vex attests GHSA-… not_affected.

    The fix proposed above (vlt's project root is the nearest ancestor vlt.json, otherwise the outermost package.json) would cover these too. If the fix stays in the listed-member path, it also needs to normalize .. (and .) in vlt patterns before matching (WorkspacesDialect::VLT / VLT_PRE_1_3), so this is a regression test either way. Repro: the #1584 layout with vlt.json {"workspaces":["packages/../other/*"],...} and the stray lock in other/o.

    (vlt does list a member outside the project root for ["../ext/*"] (workspace~..+ext+e). I left that out: from ../ext/e without an outer package.json, vlt treats that directory as its own project.)


    Generated by Claude Code

  3. mikolalysenko commented on Oct 21, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: vlt project-root lookup only recognizes listed workspace members, not vlt's nearest-ancestor vlt.json / outermost package.json root, and does not normalize .. in vlt workspace globs). Branch: agent/fix-vlt-project-root-stray-lock. Claim-ID: 2026-10-21T16:20:48Z-3b8c77


    Generated by Claude Code

  4. mikolalysenko commented on Oct 21, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1658


    Generated by Claude Code

  5. added 4 commits that reference this issue on Oct 21, 2026
    3188ed6
    363e4d9
    5550e0e
    dadf23b
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