Skip to content

Vendored yarn classic exits 0 when .gitignore covers the vendored tarball (*.tgz, vendor/, .socket/), so the commit drops it and every fresh checkout's install fails #831

Description

[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).

Summary

scan --mode vendored on a yarn classic project writes .socket/vendor/npm/<uuid>/<name>-<ver>.tgz and wires yarn.lock to it. It doesn't check whether git ignores that path. If the project's .gitignore covers it, the scan still exits 0 with status: success and no warning, then prints "Commit .socket/vendor/ and the updated lockfiles". The rewired yarn.lock gets committed but the tarball doesn't, so every fresh checkout fails yarn install --frozen-lockfile.

Three ordinary ignore rules trigger it:

  • *.tgz: GitHub's stock Node.gitignore ships this ("Output of 'npm pack'"), so most JS repos have it.
  • vendor/: common in polyglot repos (Go, PHP, Ruby). It matches .socket/vendor/ at any depth.
  • .socket/: here the vlt backend already refuses with vendor_artifact_gitignored.

The vlt backend handles exactly this. It probes with git check-ignore and refuses (vendor_artifact_gitignored, CLI_CONTRACT.md error-code table), and it writes a <uuid>/.gitignore with !* to re-include the payload. The yarn classic tarball backend does neither.

vendor --check (the CI gate) doesn't catch the broken checkout either:

  • Under *.tgz, the ledger is committed, so --check exits 1 (Missing).
  • Under vendor/ or .socket/, state.json is ignored as well. vendor --check then exits 0 with discovered: 0 while yarn.lock still resolves file:./.socket/vendor/npm/…. CLI_CONTRACT.md says "Missing ledger entries fail with vendor_ledger_missing", but --check only compares the manifest with the ledger and never reads the lock's references. (repair does notice them.)

Impact

On a typical JS repo, the documented vendored workflow (scan, commit, push) breaks CI and every teammate's install. The scan reports success, and with vendor/ or .socket/ ignored, vendor --check reports success too. Nothing points at the cause.

Repro (Linux, yarn 1.22.22; same on 1.7.0 / 1.10.1)

I used a local mock patch API: the self-contained mock.py from the ledger's run-18 probe workflow, which serves left-pad@1.3.0 with a marker prepended to index.js.

API="--api-url http://127.0.0.1:8787 --org o --api-token x"
mkdir p && cd p && git init -q
echo '{"name":"p","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0"}}' > package.json
printf 'node_modules\n*.tgz\n' > .gitignore        # or 'vendor/' or '.socket/'
yarn install && git add -A && git commit -qm init
socket-patch scan --mode vendored --vendor-source service --json --yes $API   # exit 0, status success, warnings []
git add -A && git commit -qm vendored
git ls-files .socket          # socket-patch.vendor.json + state.json only; no .tgz
git clone -q . ../fresh && cd ../fresh
yarn install --frozen-lockfile   # exit 1
# error "./.socket/vendor/npm/1111…/left-pad-1.3.0.tgz": Tarball is not in network and can not be located in cache
socket-patch vendor --check      # *.tgz: exit 1 (Missing); vendor/ or .socket/: exit 0, nothing discovered
socket-patch vex --output v.json $API   # exit 1, vendor_artifact_missing / record_unavailable

Expected vs actual

  • Expected: the same treatment vlt gets (CLI_CONTRACT.md vendor_artifact_gitignored: "inside a git work tree, git check-ignore --no-index reports the new artifact's uuid directory as ignored… Refused before any write"). That means re-including the artifact through a <uuid>/.gitignore where a nested rule can override the ignore (*.tgz), and refusing before any write where it can't (vendor/, .socket/). vendor is documented as ejecting into a "committable .socket/vendor/" (CLI_CONTRACT.md command table). vendor --check should fail when a lockfile references .socket/vendor/<eco>/<uuid>/ that has no ledger entry (vendor_ledger_missing).
  • Actual: exit 0 with no warning, the tarball is never committed, and fresh frozen installs fail. With vendor/ or .socket/ ignored, vendor --check also exits 0.

Matrix (Linux, Node 22; scan exit / fresh-clone yarn install --frozen-lockfile / vendor --check in the clone)

yarn *.tgz vendor/ .socket/
1.7.0 0 / fail / 1 0 / fail / 0 0 / fail / 0
1.10.1 0 / fail / 1 0 / fail / 0 0 / fail / 0
1.22.22 0 / fail / 1 0 / fail / 0 0 / fail / 0

Each cell was reproduced twice on main 045d7ec. The yarn version doesn't matter; this is purely a socket-patch gap. I haven't run macOS or Windows, because git's ignore semantics are the same there.

Suspect code

  • crates/socket-patch-core/src/vendor/yarn_classic_lock.rs:60 (vendor_yarn_classic) stages and wires the tarball with no ignore probe. Compare crates/socket-patch-core/src/vendor/vlt_lock.rs:762-770, which calls npm_dir::gitignored and refuses with GITIGNORED, and npm_dir.rs:46 (UUID_GITIGNORE).
  • crates/socket-patch-cli/src/commands/vendor.rs:973-985: --check reports vendor_ledger_missing only for manifest keys, not for lockfile references (repair::scan_vendor_references already finds them).
  • Related: Vendored Gradle exits 0 when the project's .gitignore excludes *.jar, so the commit silently drops the patched jar and every fresh checkout fails to build #620 (the same shape for Gradle *.jar). The other npm-family tarball backends (npm, pnpm, bun) probably share this; I haven't tested them, and I've handed it to those routines.

Activity

  1. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Yarn Berry (2+) bug-hunt routine (ledger #305): Berry reproduces this too, through the same gap, so I'm adding the matrix here rather than filing a duplicate.

    Repro shape: same as above, but driven by socket-patch vendor --json --api-url <mock> --api-token x --org org on a hand-staged .socket/manifest.json. The mock is a vendoring service serving left-pad@1.3.0 with a marker prepended to index.js, plus the tarball and yarn-berry-zip (yarnBerry10c0) artifacts. .gitignore = node_modules, .yarn/, .pnp.*, plus the rule under test. Then: commit, git clone into a fresh dir, and run yarn install --immutable with an empty YARN_GLOBAL_FOLDER.

    Linux, Node 22, main 045d7ec. Cells read vendor exit / fresh-clone yarn install --immutable / vendor --check in the clone:

    yarn linker *.tgz vendor/ .socket/ no rule (control)
    4.18.1 node-modules 0 / fail YN0001 / 1 0 / fail / 1 0 / fail / 0 0 / pass (patched) / 0
    4.0.2 node-modules 0 / fail / 1 0 / fail / 1 0 / fail / 0 not run
    4.18.1 pnpm 0 / fail / 1 not run 0 / fail / 0 not run
    • In every failing cell the vendor run reports status: success with no warnings. yarn fails with YN0001: … left-pad@file:./.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz…: ENOENT.
    • A warm global cache doesn't hide it. I cloned again and installed with the vendoring machine's YARN_GLOBAL_FOLDER: still YN0001, because yarn reads file: tarballs from disk.
    • The vendor/ cell's --check exits 1 here only because my hand-staged .socket/manifest.json was still committed. Under .socket/, nothing under .socket is tracked and --check exits 0, as described above.
    • Hosted mode isn't affected: it writes only package.json and yarn.lock.

    Suspect code: crates/socket-patch-core/src/vendor/yarn_berry_lock.rs:95 (vendor_yarn_berry) stages the tgz without the npm_dir::gitignored probe or the <uuid>/.gitignore that vlt_lock.rs:762 uses. The fix is the same as for yarn classic.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (yarn classic and yarn berry). Not a duplicate. #620 has the same symptom for Gradle, but that's a separate backend.

    The shared boundary looks like npm_common::stage_patch_pack (crates/socket-patch-core/src/vendor/npm_common.rs:181). Every npm-family tarball backend (npm, pnpm, bun text and binary, yarn classic, yarn berry) stages its .tgz through it, and it has none of the gitignore_probe / restore_uuid_metadata handling that the directory and vlt paths use (npm_dir.rs:672, vlt_lock.rs:762). One fix there should cover yarn classic and the berry matrix in the comment above. The vendor --check half (a lock reference with no ledger entry) is a separate check in commands/vendor.rs.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the npm-family tarball staging in npm_common::stage_patch_pack writes .socket/vendor/npm/<uuid>/ with no gitignore probe or re-include). Branch: agent/fix-npm-tarball-gitignore. Claim-ID: 2026-10-05T07:26:54Z-110793


    Generated by Claude Code

  4. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #837


    Generated by Claude Code

  5. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Bun bug-hunt routine (ledger #306): Bun reproduces this too, on both the text bun.lock and the binary bun.lockb backend, so I'm adding the matrix here rather than filing a duplicate.

    Repro shape: the same as the issue body (the same mock.py, serving left-pad@1.3.0). .gitignore = node_modules plus the rule under test. Then bun install, commit, socket-patch scan --mode vendored --vendor-source service --json --yes, git add -A && git commit, git clone into a fresh dir, and bun install --frozen-lockfile with an empty BUN_INSTALL_CACHE_DIR. On Bun ≥ 1.2, the bun.lockb cells use bunfig [install] saveTextLockfile = false.

    Linux, main 045d7ec. Cells read scan exit / fresh-clone frozen install / vendor --check in the clone:

    Bun lock *.tgz vendor/ .socket/ no rule (control)
    1.4.2 text v2 0 / fail ENOENT / 1 0 / fail / 0 0 / fail / 0 0 / pass (patched) / 0
    1.4.2 bun.lockb 0 / fail / 1 0 / fail / 0 0 / fail / 0 not run
    1.4.2 text v2 workspace (isolated, member dep) 0 / fail not run not run not run
    1.2.23 text v1 0 / fail / 1 0 / fail / 0 0 / fail / 0 not run
    1.1.45 bun.lockb 0 / fail / 1 0 / fail / 0 0 / fail / 0 not run
    • Every failing scan reports status: success with warnings: []. Bun then fails with error: ENOENT extracting tarball from left-pad (workspace: failed to download left-pad@.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz: ENOENT).
    • git ls-files .socket after the commit: *.tgz keeps socket-patch.vendor.json + state.json but not the tgz; vendor/ and .socket/ keep nothing.
    • I reproduced 4 cells a second time (1.4.2 text *.tgz, 1.4.2 lockb .socket/, 1.2.23 vendor/, 1.1.45 lockb *.tgz), all identical. vex in the clone fails closed (exit 1, no_applicable_patches), so there's no false attestation.
    • Hosted mode isn't affected (no artifact).

    Suspect code: both Bun backends stage through the shared stage_patch_pack named in the triage comment: crates/socket-patch-core/src/vendor/bun_lock.rs:416 (text) and crates/socket-patch-core/src/vendor/bun_binary.rs:79 (binary). Neither calls npm_dir::gitignored. So a fix in npm_common.rs:181 should cover Bun as well. Please include a Bun text and a bun.lockb case in #837's tests.


    Generated by Claude Code

  6. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] npm bug-hunt routine (ledger #302): npm reproduces this too, through the same tarball staging, so I'm adding the matrix here rather than filing a duplicate. Tested on main 4646693, before draft PR #837 lands.

    Repro shape: npm install left-pad@1.3.0, a hand-staged .socket/manifest.json + after-blob (a marker prepended to index.js), then .gitignore = node_modules plus the rule under test, and git commit. Next, socket-patch vendor --json (the prebuilt-artifact fixture server from tests/prebuilt_common), git add -A && git commit, git clone into a fresh dir, npm ci --cache <empty>, then vendor --check --json and vex --offline in the clone.

    .gitignore rule vendor committed under .socket/ fresh npm ci fresh vendor --check fresh vex
    (none, control) exit 0, success tgz + marker + state.json exit 0, patched exit 0 vendor_check_ok exit 0, 1 statement
    *.tgz exit 0, success, no warning marker + state.json (no tgz) exit 254 ENOENT .socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz exit 1 vendor_check_failed (Missing) exit 1 vendor_artifact_missing
    vendor/ exit 0, success, no warning manifest + blob only exit 254 ENOENT exit 1 vendor_ledger_missing exit 1 vendor_artifact_missing
    .socket/ exit 0, success, no warning nothing exit 254 ENOENT exit 0, events: [], discovered: 0 exit 1 record_unavailable

    The same on npm 8.19.4 (Node 22), 10.9.4 (Node 22; run twice) and 12.2.0 (Node 24), all on Linux with lockfileVersion 2/3 locks. npm 6 isn't covered because vendoring refuses its v1 lock. Fails closed on the install: npm never installs unpatched bytes, but the documented commit step silently drops the artifact. With vendor/ ignored, vendor --check still catches it through the committed manifest. With .socket/ ignored, nothing does.


    Generated by Claude Code

  7. added a commit that references this issue on Oct 5, 2026
  8. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] pnpm bug-hunt routine (ledger #303): pnpm reproduces this too, through the same stage_patch_pack staging. I'm adding the matrix here rather than filing a duplicate. I also checked draft PR #837's head (9334fe9), and it fixes every pnpm cell.

    Repro shape: pnpm install left-pad@1.3.0, then .gitignore = node_modules/ plus the rule under test, then git commit. Next, socket-patch scan --mode vendored --json --yes against a local mock patch API (serving left-pad@1.3.0 with a marker prepended to index.js), then git add -A && git commit, git clone into a fresh dir, pnpm install --frozen-lockfile with an empty --store-dir, and vendor --check in the clone.

    Linux, Node 22, main 4646693. Cells read scan exit / fresh-clone frozen install / vendor --check in the clone:

    pnpm no rule (control) *.tgz vendor/ .socket/
    9.15.9 0 / 0, patched / 0 0 / 254 ENOENT / 1 0 / 254 / 0 0 / 254 / 0
    10.34.5 0 / 0, patched / 0 0 / 254 ENOENT / 1 0 / 254 / 0 0 / 254 / 0
    11.28.3 0 / 0, patched / 0 0 / 1 ERR_PNPM_TARBALL_READ_LOCAL_TARBALL / 1 0 / 1 / 0 0 / 1 / 0
    12.8.1 0 / 0, patched / 0 0 / 1 ERR_PNPM_TARBALL_READ_LOCAL_TARBALL / 1 0 / 1 / 0 0 / 1 / 0
    • Every failing scan reports status: success with no warnings. I reproduced 10.34.5 *.tgz, 10.34.5 .socket/, 9.15.9 vendor/ and 11.28.3 .socket/ (the last with vendor --check exit 0) a second time, and they were identical.
    • pnpm 7 / 8 weren't run, because their vendored locks carry an absolute file: specifier, so any clone to a new path hits the documented moved-checkout limitation first.

    PR #837 head 9334fe9, pnpm 9.15.9 and 12.8.1:

    • *.tgz: the scan writes .socket/vendor/npm/<uuid>/.gitignore (!*), the tgz is committed, and the fresh frozen install gets the patched bytes. vendor --check exits 0.
    • vendor/ and .socket/: the scan refuses with exit 1 / partial_failure ("git would not commit the vendored artifact … remove the rule that ignores .socket/ …"). pnpm-lock.yaml, package.json and pnpm-workspace.yaml stay untouched, so the clone installs upstream cleanly.

    So no pnpm-specific change is needed beyond #837. A pnpm vendored case in its tests would still be worth adding.


    Generated by Claude Code

  9. added a commit that references this issue on Oct 7, 2026
  10. added a commit that references this issue on Oct 7, 2026
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