Repository navigation
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
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.jsontofile:.socket/vendor/...plus the stray lock, and exits 0; vexthen attestsnot_affected, including in vendored mode while the installedtools/x/node_modules/left-padis 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_affectedWith --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
- Expected: the same refusal Hosted and vendored scans from a vlt workspace member with a stray vlt-lock.json write to that lock, which vlt ignores, and exit 0 (vendored VEX then attests not_affected while
vlt cifails) #1134 gives for a listed member. CLI_CONTRACT.md'sredirect_workspace_lockfile_elsewhererow says a directory whose own locks are all ones its manager never reads is refused, and vendored refuses it withvendor_lockfile_missing, withvexreading the ignored lock as absent. vlt never readsvlt-lock.jsonin a directory with novlt.jsonbelow an ancestorvlt.json(or below an outerpackage.jsonroot when there's novlt.jsonat all), whether or not aworkspacesfield lists it. A refusal naming the vlt project root (orvlt.jsonin the directory as the way to make it standalone) would be fail-closed. - Actual: exit 0, the stray lock (and
package.jsonin vendored mode) is rewritten, and VEX attestsnot_affected.
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 consultsvlt_lock_root(:442), which finds listed-member roots (vlt_workspace_rootat:787, pluspackage.jsonworkspaces). It has no case for "an ancestorvlt.jsonexists, 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
mikolalysenko commentedon Oct 15, 2026
[agent] Triaged as
priority:p1(vlt, npm family). Confirmed onmain4cc50f4 by reading the code:member_stray_lock(crates/socket-patch-core/src/hosted/governing_root.rs~368) only treats a vlt lock as stray whenvlt_lock_rootfinds a root that lists the directory as a workspace member; there is no case for an ancestorvlt.json(vlt's project root) above an unlisted directory, andvlt_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
mikolalysenko commentedon Oct 16, 2026
[agent] vlt bug-hunt routine (ledger #307), more data on main
657fce2. A listed workspace member slips through as well when itsworkspacesglob has a..segment.vlt normalizes
..in workspace globs, bututils/workspace_globs.rsmatches components literally, apart from yarn berry's picomatch dialect. So for thesevlt.jsonworkspacesvalues, vlt records the member edge, butmember_stray_lockdoesn't see a member:workspaces["packages/../other/*"](also with./)workspace~other+oother/o["other/../packages/*"]workspace~packages+apackages/a["packages/x/../a"](packages/xabsent)workspace~packages+apackages/a["packages/*/../a"]workspace~packages+apackages/apackages/*,"packages/*"(string),{"g":"packages/*"},packages/a/,packages//a,packages/./a,packages/.*,packages/{a,.x},packages/!(a)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'spackage.jsontofile:.socket/vendor/..., after whichvlt cifails "out of sync" at both the member and the root. The copy on disk stays pristine, andvexattestsGHSA-… not_affected.The fix proposed above (vlt's project root is the nearest ancestor
vlt.json, otherwise the outermostpackage.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 withvlt.json{"workspaces":["packages/../other/*"],...}and the stray lock inother/o.(vlt does list a member outside the project root for
["../ext/*"](workspace~..+ext+e). I left that out: from../ext/ewithout an outerpackage.json, vlt treats that directory as its own project.)Generated by Claude Code
mikolalysenko commentedon Oct 21, 2026
[agent] Claiming this issue (shared root cause: vlt project-root lookup only recognizes listed workspace members, not vlt's nearest-ancestor
vlt.json/ outermostpackage.jsonroot, and does not normalize..in vlt workspace globs). Branch: agent/fix-vlt-project-root-stray-lock. Claim-ID: 2026-10-21T16:20:48Z-3b8c77Generated by Claude Code
mikolalysenko commentedon Oct 21, 2026
[agent] Draft PR: #1658
Generated by Claude Code
Refuse vlt stray locks under vlt's project root
Document vlt project-root stray lock refusal
Let vlt's project root win over listings
Merge main into agent/fix-vlt-project-root-stray-lock