Skip to content

Vendored vlt scan accepts a package with a preprepare or postprepare script, which vlt ci then runs inside the committed vendor directory, so vendor --check and vex fail and repair loops #1633

Open

Description

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

Summary

Vendored vlt mode refuses a package that vlt build would build inside the committed file: directory (vendor_vlt_build_scripts_unsupported). The check lists only preinstall, install, postinstall, prepare and binding.gyp. vlt also runs preprepare and postprepare for a file: directory dependency, and each one runs even when it is the package's only script. vlt's own nodeNeedsBuild tests scripts.prepare || scripts.preprepare || scripts.postprepare, and its --allow-scripts help lists all six scripts.

So a registry package whose only lifecycle script is postprepare (or preprepare) gets vendored with exit 0. While it came from the registry, vlt never ran that script, because registry nodes aren't "prepable". Once vendored, the next vlt ci / vlt install with an allow-scripts query that matches the package runs the script inside .socket/vendor/npm/<uuid>/<name>-<ver>/node_modules/<name>/. That rewrites the committed artifact, which is exactly what the refusal exists to prevent.

Impact

  • vendor --check exits 1 with vendor_inventory_mismatch right after an ordinary vlt ci.
  • vex exits 1 and leaves the package out ("the vendored artifact's contents do not match its ledger inventory"), even though the installed copy is patched. The project loses its attestation.
  • The fix the CLI prescribes loops. socket-patch repair redownloads the artifact and vendor --check goes green, but the next vlt ci runs the script again and the check is red again.
  • The script's output lands in the committed tree: an untracked file, or a modified tracked file when the script edits the package. The git checkout is dirty after every install.

Repro (main 7d3086f, also release 5.0.1; vlt 1.4.0)

I used the run-19 mock from #1494, changed so left-pad@1.3.0's package.json (in both the pristine and the patched tarball, and in the packument) carries "scripts": {"postprepare": "node -e \"require('fs').writeFileSync('built.txt', require('./index.js'))\""}. The patch only changes index.js. Start it on :18555 (registry) and :18556 (patch API).

export SOCKET_API_URL=http://127.0.0.1:18556 SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18556 \
       SOCKET_NPM_REGISTRY=http://127.0.0.1:18555 SOCKET_ORG_SLUG=test-org SOCKET_API_TOKEN=fake
mkdir p && cd p
echo '{"name":"root","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
echo '{"config":{"registries":{"npm":"http://127.0.0.1:18555/"},"allow-scripts":"#left-pad"}}' > vlt.json
vlt install
find node_modules/.vlt -name built.txt | wc -l      # 0: vlt doesn't run postprepare for a registry node
socket-patch scan --mode vendored --yes --json      # exit 0, applied (no refusal)
git init -q && git add -A && git commit -qm vendored
vlt ci                                              # exit 0, runs postprepare in the vendor dir
git status --porcelain --ignored .socket            # ?? .socket/vendor/npm/<uuid>/left-pad-1.3.0/node_modules/left-pad/built.txt
socket-patch vendor --check                         # exit 1: vendor_inventory_mismatch; "run `socket-patch repair`"
socket-patch vex -O vex.json                        # exit 1: omits pkg:npm/left-pad@1.3.0
socket-patch repair --yes && socket-patch vendor --check   # exit 0
vlt ci && socket-patch vendor --check               # exit 1 again

Control: the same package with prepare instead of postprepare is refused, vendor_vlt_build_scripts_unsupported (exit 1, nothing written). That's the documented behaviour.

vlt itself, outside socket-patch: a file:./x dependency whose package.json has only a preprepare or only a postprepare script runs it on vlt install --allow-scripts '*' on 1.0.10, 1.2.0, 1.3.8 and 1.4.0 (the script's marker file is written each time).

Expected vs actual

  • Expected (docs/ecosystems.md, vlt notes, "Vendored: directory artifacts"): "a package with a preinstall, install, postinstall or prepare script or a binding.gyp, which vlt build would build inside the committed directory (vendor_vlt_build_scripts_unsupported); use hosted mode for those." The point of the rule is that nothing vlt build runs may touch the committed directory. A preprepare / postprepare package should get the same refusal, and the docs should name those scripts too.
  • Actual: vendored, exit 0. The first vlt ci with matching allow-scripts corrupts the artifact, and vendor --check / vex / repair then fail or loop.

Hosted mode is fine with the same package: registry nodes never run prepare-family scripts, vlt ci installs the patched bytes, and rollback is byte-exact.

OS × version

OS vlt postprepare only preprepare only prepare (control)
Linux 1.2.0 fail (vendored, vlt ci rewrites the artifact, check / vex exit 1) not run not run
Linux 1.3.8 fail not run not run
Linux 1.4.0 fail (3×; repair loop shown) fail refused (pass)
Linux 1.4.0 + release 5.0.1 fail not run not run

I didn't probe macOS or Windows, because probe branches are blocked for this routine (branch deletion is denied). The script list is OS-independent code.

First bad version

It's present since the build-script refusal was added (in release 5.0.1 and on main). I couldn't bisect further, because main's history was squashed.

Suspect code

  • crates/socket-patch-core/src/vendor/npm_dir.rs:757, vlt_build_steps: the list ["preinstall", "install", "postinstall", "prepare"] is missing preprepare and postprepare. vlt's nodeNeedsBuild (src/query/src/pseudo/scripts.ts in the vlt bundle) checks prepare || preprepare || postprepare for prepable nodes.
  • npm_dir.rs:1161, the test vlt_build_steps_name_every_script_vlt_build_runs_in_place, pins the incomplete list.
  • docs/ecosystems.md vlt notes: the refusal sentence lists the same four scripts.

Activity

  1. mikolalysenko commented on Oct 17, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (vlt). Confirmed on main 7d3086f: vlt_build_steps (crates/socket-patch-core/src/vendor/npm_dir.rs:759) lists only preinstall, install, postinstall, prepare, so a package whose only lifecycle script is preprepare or postprepare passes the vendor_vlt_build_scripts_unsupported refusal. The test vlt_build_steps_name_every_script_vlt_build_runs_in_place and the vlt notes in docs/ecosystems.md pin the same four scripts. No duplicate or existing fix PR found.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 18, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: vlt_build_steps omits the preprepare / postprepare scripts vlt runs for a file: directory). Branch: agent/fix-vlt-prepare-family-scripts. Claim-ID: 2026-10-18T00:20:50Z-5e20ac


    Generated by Claude Code

  3. mikolalysenko commented on Oct 18, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix: #1637


    Generated by Claude Code

  4. added 2 commits that reference this issue on Oct 18, 2026
    fd3b3f0
    8ab8b12
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