Skip to content

Never patch a pnpm, yarn or vlt dependency the user patched - #2018

Merged
Mikola Lysenko (mikolalysenko) merged 27 commits into
mainfrom
fix/user-patched-deps-pnpm-yarn-vlt
Nov 9, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 27 commits into
mainfrom
fix/user-patched-deps-pnpm-yarn-vlt

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2010 (fix/npm-discovery-false-attestation).

Owner decision: by default, a dependency the user patched themselves is not patched in any mode, for any package manager. Only an explicit user override patches it.

What was already there (and what wasn't)

The CLI's consent gate (patch::user_patches::UserPatchIndex, #1646) already detects pnpm patchedDependencies, yarn berry patch: and patch-package for every npm-family project. Unless --allow-patch-conflicts is passed, it keeps scan (all modes), get, vendor, apply and the hosted/vendored writers from wiring those packages. Two consumers did not use that evidence:

  1. vex discovery attested a pnpm, yarn, vlt (or npm/Bun) pin of a self-patched package. That covers a pin written before the patch was declared, and a pin that an --allow-patch-conflicts run wrote.
  2. The in-memory hosted engine (Node addon / hosted-bundle) has no consent gate in front of it, so it rewired those packages.

One detector, three consumers

UserPatchIndex has a single walk over a ProjectView (disk, snapshot or memory). The CLI consent gate (discover()) runs it on disk and reads every ecosystem. Discovery and the hosted engine (npm_family_in()) run it on any view and read only the npm-family declarations.

Manager What is detected Where
pnpm patchedDependencies keys (name, name@version, name@range) package.json pnpm.patchedDependencies, pnpm-workspace.yaml patchedDependencies, each pnpm lock's top-level patchedDependencies map and (patch_hash=…) key suffixes (root, Rush, members, and a ref's own lock)
yarn berry user patch: descriptors (Yarn's builtin<compat/…> patches are excluded), every layer peeled, aliases resolved package.json dependency maps and resolutions; patch: locators that yarn.lock resolves, including one that wraps the hosted tarball (#962)
yarn classic no patch protocol of its own; patch-package patches/<name>+<version>.patch, or a --patch-dir that a package.json script names
vlt no patch mechanism (see below); patch-package same as yarn classic

The walk reads the project, its declared workspace members, and (on disk) the ancestor workspace settings up to the git repository root.

Repository bound, workspace runs only. In a workspace run the walk never reads outside the git repository root; in memory, the project is its own root. A workspace member or literal --patch-dir beyond that root is not read. Instead it protects every npm package with an "outside the git repository" diagnostic, in the gate too.

  • In a global run (--global / -g / --global-prefix) the walk has no repository bound. It reads members and patch directories wherever they are, and they protect only the packages they patch.
  • The scope is ReadScope { Workspace, Global }, chosen once from the CLI flags via GlobalArgs::read_scope() and passed through UserPatchIndex::discover_scoped. It has the same shape as Never read or rewrite project files outside the git repository root #2025's utils::repo_root::ReadScope (same variants, of_global) and is meant to be replaced by that type when the two branches meet.
  • A global run still inspects no project: since fix(cli)!: require consent before patching user-patched packages #1646, a local checkout does not govern the global tree. So today the Global scope is only reachable through the core API.
  • vex discovery and the in-memory engine only run on workspaces and keep the bound.

The npm-family scope leaves out npm's and Bun's own top-level patchedDependencies and npm's lock-entry patched records. No other package manager reads them, and #711, #367 and #2010 already judge them per resolution.

vlt evidence: vlt@1.6.2 (latest, published 2026-11-03) contains no patchedDependencies, no patch_hash and no patch command. Its vlt.json modifiers rewrite dependency specs, not package contents. A vlt project can therefore only patch through a postinstall tool such as patch-package, and that is what gets read.

Behavior per mode

  • Discovery: a new vex::discover::user_patched pass marks every npm ref the index protects as UnattestedKind::UserPatchedDependency. Like Stop attesting npm-family pins pnpm or a user patch overrides #2010's npm and Bun marks, each mark is an UnwiredCopy with CopyTarget::Ref. It therefore lands only on a ref that survives the lock contests, and a ref already marked keeps its first reason. The ref stays, so rollback, remove and list still find it.
    • How "is it Socket's bytes?" is decided: while a user patch is declared for that name@version, the package manager or its postinstall applies it on top of whatever tarball the lock names. The install therefore never runs the Socket patch alone, and the ref is never attested, with or without consent.
  • Bun and npm keys naming the pinned tarball (a patch made on top of the Socket pin, On Bun 1.4, a bun patch made on a hosted or vendored package is silently dropped by the next superseding re-run, takeover, rollback or revert (the #367 guard only matches name@version keys) #1019) are now marked too.
    • The npm and Bun extractors share one helper, Stop attesting npm-family pins pnpm or a user patch overrides #2010's npm::mark_user_patched (via user_patch_source). It now reads keys against no target, so any key for that name@version counts, and that case gets its own detail.
    • The Stop attesting npm-family pins pnpm or a user patch overrides #2010 test that asserted the old attested behavior now asserts the mark.
    • The hosted rewriter is unchanged. If the key names the URL this run would pin (an in-sync re-run), it keeps the pin and confirms it. If the key names an older URL, it refuses with redirect_bun_patched_dependency_skipped. Either way vex's plan omits the pin through the mark.
  • Hosted: engine::withhold_user_patched removes such candidates from every rewriter and records each as a skipped row with reason user_patch_conflict (the consent gate's code). In memory it also adds a warning.
    • The in-memory engine always applies this.
    • The CLI disk flow applies it unless --allow-patch-conflicts is passed. There it mostly duplicates the consent gate, but it also reads the Rush lock, which the gate does not.
  • Vendored: no change. Every vendored entry point already runs the consent gate:
    • vendor and the vendored writers refuse with user_patch_conflict.
    • scan --mode vendored and apply skip the package.

The override

The override is the existing invocation-only --allow-patch-conflicts. It has no env or policy binding, and no new surface is added.

  • It already let pnpm, yarn and patch-package packages through the consent gate. It now also lifts the engine gate.
  • With it, hosted and vendored mode wire the package, under the user's patch. vex still never attests the package while the patch is declared.
  • npm and Bun native patches stay refused even with consent, as before (CLI_CONTRACT: "Backend safety refusals still apply").

CLI_CONTRACT.md (Existing project patches, Unattested references, the vex_npm_patched_dependency row) and docs/usage.md are updated.

Tests

Verified to fail before:

  • vex::discover::user_patched::tests:
    • pnpm: workspace, package.json pnpm field, lock map, patch_hash key, range and bare keys.
    • yarn berry: package.json patch: wrapping the hosted URL; a yarn.lock patch locator.
    • patch-package: yarn classic, vlt hosted, vlt vendored and npm pins, including --patch-dir.
    • Workspace members, on disk and in memory, agree with the gate; a member beyond the repository root is not read.
    • A contested ref leaves no mark.
  • bun_user_patched_dependency_pin_is_not_attested (key for the pin's own tarball).
  • hosted_memory_engine::user_patched_npm_packages_are_never_pinned.
  • in_process_redirect_pnpm::hosted_pins_a_pnpm_patched_dependency_only_with_explicit_consent:
    • default: skipped;
    • --allow-patch-conflicts: pinned;
    • discovery: the pin stays a ref and is marked unattested.
  • patch::user_patches::tests::npm_family_view_index_agrees_with_the_consent_gate.

Runs:

  • cargo test -p socket-patch-core --lib -- vex::discover:: patch::redirect:: vendor::pnpm vendor::yarn vendor::vlt vendor::npm vendor::bun hosted:: formats::pnpm formats::yarn patch::user_patches: 2523 passed. This includes the new the_repository_bound_holds_for_workspace_runs_only test: in a workspace run, an out-of-repo member or patch dir protects everything; in a global run, it is read and protects only the packages it patches. This includes committed_fixture_corpus_matches_golden, with no golden changes.
  • Core integration targets: hosted_inventory 21, pnpm_hosted 6, vlt_locks 10, upstream_restore_golden 53.
  • CLI: in_process_redirect_pnpm 42, in_process_redirect 152, hosted_memory_engine 50, hosted_memory_parity 43, user_patches 27, vex 48, e2e_vex_redirect 53, and lib vex_sources / user_patches / scan::hosted 63. All pass.
  • cargo clippy --locked --workspace --all-features -- -D warnings and cargo fmt --all -- --check are clean.

Review findings (/code-review high)

All ten findings were confirmed. Each is fixed in 93709da82.

  1. pnpm member manifest selected presence-only: confirmed and fixed. Under named roots (projectRoots: [""]), a pnpm member with its own lock is not a root of its own. Its package.json was therefore selected presence-only, read as unknown metadata, and every npm candidate was withheld. With detected roots the member is a root and gets content, which is why the existing parity test missed it.
    • The rule now: selection asks for content for every file the walk can read.
    • A file the walk reads that the host supplied without content (oversize, LFS) still protects every npm package. A missing file must never silently disable protection.
    • The one safe not-applicable case is a --patch-dir that has no listing in memory. Selection supplies every *.patch, so such a directory holds none.
  2. --patch-dir contents never selected: confirmed and fixed. Selection now requests every *.patch file by name (presence-only is enough, since only the file names are read).
  3. patch-package files and npm/yarn member manifests never fetched: confirmed and fixed. This was failing before: an npm workspace member's patches/left-pad+1.3.0.patch → memory pinned left-pad. Selection now fetches every package.json, pnpm-workspace.yaml, pnpm-lock.yaml and yarn.lock, and every *.patch by name, outside node_modules, .git, .socket, target and .venv. That list is a constant (WALK_SKIP) shared with the walk.
    • Test: hosted_memory_parity::user_patches_are_selected_and_agree_with_the_disk_flow drives select_paths → memory project → engine for 8 cases (pnpm member manifest, npm member patches/, npm member patch: descriptor, root patches/, root --patch-dir, and three no-patch controls). Each case runs with both detected and named roots and is compared with the disk flow.
  4. Out-of-repo go.work member recorded under npm: confirmed and fixed. Every pending directory now carries the ecosystem whose declaration named it. An out-of-repo go.work use member protects Go modules (fail closed for Go), not npm packages. Test: an_out_of_repo_go_member_protects_go_not_npm.
  5. Ancestor markers checked on the lexical parent, read via ..: confirmed and fixed. Ancestors now come from utils::repo_root::search_dirs and are checked and read at the same canonical absolute path. Test: a_symlinked_root_reads_its_real_ancestors.
  6. Every absolute path counted as outside the repo: confirmed and fixed. An absolute path is now normalized lexically and accepted when it lies under the repository root. Test: an_absolute_patch_dir_inside_the_repository_is_read.
  7. The disk hosted flow re-walked the project: confirmed and fixed. The CLI now withholds by the Rush lock alone (UserPatchIndex::from_locks + engine::withhold_user_patched_by). The consent gate has already walked the project.
  8. Root locks parsed twice: confirmed and fixed. npm_family_in skips a passed pnpm-lock.yaml or yarn.lock whose directory the walk already read.
  9. vlt preflight downloaded before the withhold: confirmed and fixed. The CLI withhold now runs before vlt::artifact_preflight. The in-memory preflight is offline and downloads nothing.
  10. Hand-rolled git-root search and lexical_join: confirmed and fixed. They are replaced by find_git_repo / search_dirs (which honor GIT_CEILING_DIRECTORIES and the home rule) and normalize_lexically_keeping_escapes. The minimal ReadScope stays, to be replaced by Never read or rewrite project files outside the git repository root #2025's.

Two caveats on the tests:

  • Findings 1–3 were confirmed by running the new test against the previous commit. Findings 4–6 are confirmed by reading the previous code; each new test asserts behavior that code could not produce.
  • Selecting every nested package.json and lock adds fetches for large monorepos. That is the cost of reading members' declarations at all. The host's max_files limit still applies and fails the run closed.

Not done

  • No core-level vendored gate for pnpm or yarn. The CLI consent gate covers every vendored entry point. A core gate would need the override plumbed into the vendor backends.
  • Hosted still pins a package whose key names the pin's own tarball (see above). Only vex omits it.
  • The in-memory engine sees a member root's files only. It cannot read the workspace settings of an enclosing root that the host splits off.

Brought current with #2010, plus round-2 review findings (cloud routine)

Merged the updated #2010 in. It merged cleanly, and the targeted core tests pass (764).

Review findings, round 2 (/code-review high on 93709da)

  1. The disk flow no longer withheld packages a root shrinkwrap.yaml records as patched: fixed. The consent gate's walk never reads that file, while the in-memory engine does. The disk gate now reads it as well as the Rush lock. Since pnpm ≤2 had no patches, the practical impact is small, but the two flows agree again.
  2. The ancestor walk went past home: fixed. repo_root::search_dirs stops only exactly at $HOME. A project outside home with no checkout therefore read /package.json and similar files. The old stop at home or any directory above it is restored, per owner decision 6: without a checkout, upward lookups behave as before.
  3. The snapshot fingerprint lost the root's .git: fixed. It's covered again, since it decides the read bound.

None of the three has a new test yet. Finding 2 needs a HOME override, which isn't safe in parallel lib tests. Findings 1 and 3 need a disk-flow CLI fixture. The existing user_patches CLI suite (27) and the core suites pass.

Declined or deferred:

Tests (cloud run): cargo test -p socket-patch-core --lib -- user_patch vex::discover:: hosted:: utils::repo_root gives 687 passed. CLI --test user_patches gives 27 passed. clippy and fmt are clean.


Note

Medium Risk
Changes hosted-scan candidate filtering, vex attestation, and user-patch discovery across npm-family locks and monorepo boundaries; incorrect reads could skip or over-block pins, but behavior is intentionally fail-closed.

Overview
Self-patched npm-family dependencies (pnpm patchedDependencies, Yarn patch: descriptors, patch-package) now follow the same rules as npm/Bun patchedDependencies: by default hosted scan skips them as user_patch_conflict, vendored/get still refuse without consent, and vex never attests them while the user patch is declared—even after an --allow-patch-conflicts run wires the pin.

UserPatchIndex is refactored into one shared walk (discover_scoped / npm_family_in) with ReadScope::Workspace vs Global and a git-repository read bound (out-of-repo workspace members or patch dirs fail closed instead of being read). The in-memory hosted engine always applies withhold_user_patched (no consent knob); disk scan supplements the consent gate with Rush/subspace and root shrinkwrap.yaml locks. Path selection now fetches nested manifests, locks, and *.patch files the walk needs so memory and disk stay aligned.

Documentation and user-facing notes broaden vex_npm_patched_dependency to the full npm family; Bun/npm patches layered on the Socket tarball are omitted from attestation with updated discovery detail.

Reviewed by Cursor Bugbot for commit 092feb1. Configure here.

/code-review high findings not fixed in this PR

Fixed at e0cc402: out-of-root ancestor/member reads refused by the view bound (two failing unit tests), Global scope not reaching out-of-repo members, discovery dropping unreadable-lock errors, the engine treating an in-memory symlinked lock as unknown, and doubled periods in CLI_CONTRACT.md. Left for follow-ups, with reasons:

  • Rush / root shrinkwrap.yaml evidence lives only in the hosted disk-scan supplement, not the shared UserPatchIndex::discover gate used by vendor/get/apply. That gap predates this PR (the gate never read Rush locks); moving it into the walk's Gate scope is a separate change. (The supplement is now re-read under the apply lock, 3ab7c1993.)
  • user_patch_need in hosted memory selection streams every nested package.json/yarn.lock/pnpm file regardless of root ecosystem, which can push large monorepos toward the selection limits. Owner decision (2026-11-09): keep the over-fetch and fail closed (Option A).
  • Root locks are read twice (explicit list plus the walk) and the disk supplement reads Rush locks even with no npm candidate: performance only, no correctness impact.
  • npm discovery's warning detail can name the pin's own patchedDependencies key when Bun has keys for two tarballs of one version: wording of a warning only.

Review round 3 (3ab7c1993)

These come from Tanmay's thread, Wenxin's review, the owner decisions and /code-review high on the delta. The details are in the follow-up comment.

  • patch-package: evidence protects every version of the package, since patch-package applies a patch to whatever version is installed. Patch directories are walked recursively, so @scope/name+1.0.0.patch counts. The walk follows linked subdirectories and skips node_modules/.git/etc.
  • In-memory linked package.json: fails closed. The exception is the root manifest beside a yarn berry lock, which the engine refuses as a link.
  • Rush / shrinkwrap.yaml supplement: re-read under the apply lock. Its reader moved into core (engine::withhold_user_patched_by_supplement) and reads a lock that links out of the repo through the link.
  • CLI_CONTRACT: a symlinked project file is read through its link (owner decision). The *.patch selection wording and the every-version rule are updated.

Generated by Claude Code

VEX discovery attested two kinds of npm-family pins that the hosted and
vendored writers refuse, so the statement could cover bytes the install
never uses.

N1, pnpm gitBranchLockfile (#556): with the setting on and a
pnpm-lock.<branch>.yaml present, pnpm installs the branch from that lock.
Hosted withholds the root and member locks and refuses every pnpm pin;
vendored refuses the project. Discovery now runs the same detection
(utils::pnpm_workspace::git_branch_locks, via a new git_branch_locks_in
that skips the environment, as discovery already does for the lockfile
setting) and, only while the lockfile setting is on and outside Rush,
marks every ref of the root and member pnpm locks
UnattestedKind::PnpmGitBranchLock.

N2, user-patched npm / Bun packages (#711, #367): hosted leaves a package
the project patches itself unwired and never confirms it; vendored
refuses. Hosted's npm gate is factored into patch::redirect::NpmUserPatches
(package.json patchedDependencies keys plus non-null lock-entry `patched`
records in any present npm lock), which the hosted rewriter and npm
discovery now share. Bun discovery applies the rewriters' per-dependency
rule (bun_lock_text::patched_dependency_key over the manifest keys, plus
bun.lock's mirrored keys for the text lock) against each ref's hosted URL
or vendored tarball. Matching refs are marked
UnattestedKind::UserPatchedDependency.

Both keep the ref (rollback / remove / list still find a pre-fix pin and
the ledgers keep the wiring live, since no re-run can clear the evidence)
and only vex omits it, as vex_pnpm_git_branch_lockfile /
vex_npm_patched_dependency. CLI_CONTRACT.md documents both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Discovery never reads the environment for pnpm settings: the
  files-only flag now reaches `sharedWorkspaceLockfile` too
  (view_setting / workspace_members / new member_locks_in), and
  discovery's member-lock listing uses it.
- A governing ancestor pnpm-workspace.yaml is read in files-only mode
  only from inside the project's git repository (stat-only check), so
  discovery never reads project files above the repository root.
- User-patched marks are deferred as an UnwiredCopy for exactly that ref
  (new CopyTarget::Ref), so a ref a later contest drops keeps no stale
  reason. npm and Bun share one helper (mark_user_patched).
- NpmUserPatches records keys only bun.lock mirrors, and the detail
  names that file instead of package.json.
- A ref with no url or vendored artifact is still matched by
  name@version.
- npm locks are walked for `patched` records only when their bytes
  spell `"patched"`; gitBranchLockfile detection runs only once a root
  or member pnpm lock wired a patch.
- The branch-lock mark names the branch lock in the marked lock's own
  directory.
- The vex_npm_patched_dependency remedy names `socket-patch rollback
  <uuid>`; CLI_CONTRACT.md says the tarball-key rule applies to npm and
  Bun alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision for #556 in VEX discovery: a `pnpm-lock.<branch>.yaml` at
the root, or in a workspace member that installs from its own lock,
withholds every root and member pnpm lock pin from attestation whatever
`gitBranchLockfile` (or pnpm's `lockfile` setting) says. The check is
deterministic and fail-closed, reads the project's files only (no
environment, no settings, nothing above the project), and runs only once
one of those locks wired a patch.

New `utils::pnpm_workspace::git_branch_lock_files` lists the branch
locks without reading the setting. The mark keeps the
`PnpmGitBranchLock` kind and still names the branch lock in the marked
lock's own directory. The detail, the vex remedy and CLI_CONTRACT.md now
say a branch lock's presence withholds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A package the project patches itself is applied by the package manager
on top of whatever tarball the lock names, so a Socket pin of it either
fails the install or installs the user's diff stacked on the Socket
patch. The CLI's consent gate (patch::user_patches::UserPatchIndex, #1646)
already kept `scan` / `get` / `vendor` / `apply` from wiring such pnpm,
yarn berry and patch-package packages unless --allow-patch-conflicts, but
two consumers did not read that evidence:

- vex discovery attested a pnpm, yarn, vlt (or npm / Bun) pin of such a
  package: a pin written before the patch was declared, or one an
  --allow-patch-conflicts run wrote.
- the in-memory hosted engine (Node addon, hosted-bundle) has no gate in
  front of it and rewired them.

One detector, three consumers. UserPatchIndex's pnpm / yarn /
patch-package parse rules are factored into pure helpers (pnpm_yaml,
yarn_manifest, yarn_lock, patch_package_dirs, patch_file), which the
consent gate keeps using, plus UserPatchIndex::npm_family_in, which reads
the same declarations through a ProjectView (disk, snapshot or memory):
package.json's pnpm.patchedDependencies, yarn patch: descriptors and
patch-package dirs, pnpm-workspace.yaml's patchedDependencies, the
patches of the given pnpm locks (top-level map, patch_hash suffixes) and
yarn.lock's user patch: locators. It leaves out npm's and Bun's own
top-level patchedDependencies, which no other package manager reads and
which #711 / #367 / #2010 already judge per resolution.

- Discovery: a new vex::discover::user_patched pass marks every npm
  ref the index protects UnattestedKind::UserPatchedDependency (the ref
  stays, so rollback / remove / list still find it). The bytes the
  install runs are Socket's patch alone only when no user patch is
  declared, so a declared one always withholds the statement, with or
  without consent.
- Hosted: engine::withhold_user_patched drops such candidates from every
  rewriter and records them skipped `user_patch_conflict` (the consent
  gate's code), plus a warning in memory. The in-memory engine always
  applies it; the CLI disk flow applies it unless --allow-patch-conflicts
  (it adds the Rush lock the consent gate does not read).
- Vendored: unchanged. Every vendored entry point already runs the
  consent gate, which refuses (`vendor`, vendored writers) or skips
  (`scan`, `apply`) these packages.

Override: the existing --allow-patch-conflicts, invocation-only. It
already covered pnpm / yarn / patch-package at the consent gate and now
also lifts the engine gate; vex never attests while the patch is
declared. npm / Bun native patches stay refused even with consent, as
before.

vlt has no patch mechanism (vlt@1.6.2 has no patchedDependencies,
patch_hash or `patch` command; vlt.json modifiers rewrite specs), and
neither has yarn classic, so patch-package is what is read for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two follow-ups under the rule "a user patch applied on top means never
attest".

Bun (and npm) keys naming the pinned tarball itself. #2010 kept attesting
a pin whose `patchedDependencies` key names the pin's own tarball (a `bun
patch` made on top of the Socket pin, #1019), because discovery reused
the hosted rewriter's per-dependency rule, where such a key is no
conflict. Bun applies that patch over the Socket bytes, so the install
never runs the Socket patch alone. The shared npm / Bun helper
(npm::user_patch_source, behind mark_user_patched) now reads the keys
against no target (any key for the name@version counts) and flags the
case where only a key for the ref's own tarball matches, which gets its
own detail; npm and Bun refs alike. bun_user_patched_dependency_pin_is_not_attested pinned the
old attested behavior and now asserts the mark. The hosted rewriter is
unchanged: it keeps such a pin (and confirms it on an in-sync re-run),
while vex's plan omits it through the unattested mark.

Workspace members. The consent gate walked workspace members and
ancestor workspace settings; discovery and the in-memory engine read the
project root only. UserPatchIndex's walk now runs over a ProjectView and
is shared: discover() (the gate) is the walk over the disk with every
ecosystem, npm_family_in() the same walk over any view with the
npm-family declarations (pnpm, yarn, patch-package), so members'
package.json pnpm.patchedDependencies, yarn patch: descriptors, patches/
folders and --patch-dir, and their pnpm/yarn files are read alike. The
member glob walk is rewritten over the view (same depth, skip list,
limits and link handling as the walkdir one), and an ancestor with any
workspace marker is walked in both scopes.

The walk never reads outside the git repository root (the nearest
ancestor holding .git, inclusive; in memory the project itself): the
ancestor walk stops there, as before, and a workspace member or literal
--patch-dir beyond it now protects every npm package with an "outside
the git repository" diagnostic instead of being read, in the gate too.
Without a .git the gate's home-directory rule still bounds the ancestor
walk. workspace_member_patches_agree_with_the_consent_gate checks
discovery and the gate agree on member cases, on disk and in memory, and
on a member beyond the repository root.

Contest survivors only. Like #2010's npm / Bun marks, the pnpm / yarn /
patch-package pass now records each mark as an UnwiredCopy for exactly
that ref (CopyTarget::Ref), so it lands only on a ref that survives the
lock contests and a ref an earlier extractor already marked keeps that
first reason; a_contested_user_patched_ref_leaves_no_mark covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner clarification: the repository-root bound is for workspaces; a
global run should be able to read anywhere on the system. The shared
user-patch walk now takes a ReadScope { Workspace, Global }, chosen once
from the CLI mode (GlobalArgs::read_scope, from --global / -g /
--global-prefix), and only a Workspace walk bounds its reads: a
workspace member or --patch-dir beyond the git repository root protects
every npm package instead of being read. A Global walk reads both
wherever they are and protects exactly what they patch. The ancestor
walk itself (stop at .git, never reach the home directory) is the same
in both scopes.

UserPatchIndex::discover stays the workspace walk;
UserPatchIndex::discover_scoped takes the scope, which the CLI's
inspect() now threads through. A global run still inspects no project
(a local checkout does not govern the global tree, #1646), so the
Global arm is reached through the core API today. vex discovery and the
in-memory hosted engine only run on workspaces and keep the bound.

ReadScope has the shape of #2025's utils::repo_root::ReadScope (same
variants and of_global), so it is swapped for that type when the two
branches meet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In-memory selection (the engine's view holds only what select_paths asks
the host for):
- A workspace member's package.json was selected presence-only (a pnpm
  member under named roots) or not at all (npm / yarn members), and
  patch-package files and a --patch-dir's contents never: the engine
  either withheld every npm package over a member it saw without
  content, or pinned a package a member or patches/ file patches.
  Selection now asks, outside node_modules / .git / .socket / target /
  .venv (WALK_SKIP, shared with the walk), for every package.json,
  pnpm-workspace.yaml, pnpm-lock.yaml and yarn.lock with content and
  every *.patch file by name. A file the walk reads that the host
  supplied without content still protects every npm package (never a
  silent miss); a required --patch-dir with no listing in memory holds
  no *.patch (selection supplies them all), so it no longer withholds
  everything. user_patches_are_selected_and_agree_with_the_disk_flow
  drives select_paths -> engine for each case, with detected and named
  roots, against the disk flow.

The walk:
- Ancestors come from utils::repo_root::search_dirs (git's lookup,
  honoring the home rule and GIT_CEILING_DIRECTORIES), checked and read
  at the same canonical absolute path, and the repository root from
  find_git_repo; the hand-rolled search and lexical_join are gone
  (utils::relpath::normalize_lexically_keeping_escapes).
- The bound resolves absolute paths: an absolute --patch-dir or member
  inside the repository is read.
- An out-of-repo directory is recorded under the ecosystem that named
  it: a go.work use member protects Go modules, not npm packages.
- npm_family_in parses a passed lock only when the walk did not already
  read that directory's lock.

Hosted disk flow: the engine gate no longer re-walks the project after
the consent gate; it withholds by the Rush lock alone
(UserPatchIndex::from_locks + engine::withhold_user_patched_by), and
before the vlt artifact preflight, so a withheld artifact is never
downloaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
git_branch_lock_files ignored Members::Unresolved, so with the shared
lock off and no readable member list, discovery attested the root pin
even though a member's pnpm-lock.<branch>.yaml could not be ruled out.
Hosted mode already treats that case as a possible branch lock; return
the reason and withhold the same way (fail-closed). Addresses the
Bugbot finding on pnpm_workspace.rs.

Co-Authored-By: Claude <noreply@anthropic.com>
Second review round. The `PnpmGitBranchLock` doc said the mark applies
"while gitBranchLockfile is on and a branch lock exists"; discovery
marks on a branch lock's mere presence (it reads no environment) and
when the members that could hold one cannot be listed. The doc and the
CLI note now say so, and the note names the remedy for unlisted members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion' into fix/user-patched-deps-pnpm-yarn-vlt
Second review round on the user-patch walk.

- The disk hosted flow's extra user-patch gate read only the Rush lock,
  assuming the consent gate's walk covers the rest; that walk never
  reads a root `shrinkwrap.yaml`, which the in-memory engine does, so
  the two flows disagreed on a package it records as patched. The disk
  gate reads it too.
- The ancestor walk moved to `repo_root::search_dirs`, which stops only
  exactly at `$HOME`; the old walk stopped at home or any directory
  above it, so a project outside home without a checkout now read
  `/package.json` and friends (owner decision 6: without a checkout,
  upward lookups behave as before). The old stop is restored.
- The snapshot fingerprint covers the project root's own `.git` again,
  which decides the read bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with #2011 (Go workspace override): keep both sides'
UnattestedKind variants (PnpmGitBranchLock, UserPatchedDependency and
GoWorkspaceOverride with its file_scoped()), both sets of vex_sources
notes and tests, and both CLI_CONTRACT bullets (the unattested-references
bullet now counts seven cases).

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
#	crates/socket-patch-cli/src/commands/vex_sources.rs
#	crates/socket-patch-core/src/vex/discover/mod.rs
Main's #2007 added the vex_nuget_lock_other_version note beside this
branch's npm-family notes; keep both, and name NuGet's case next to
Gradle's in the contract's Eight-cases sentence as main now does.

Co-Authored-By: Claude <noreply@anthropic.com>
…pnpm-yarn-vlt

The base now carries main's #2013, which deleted the vlt artifact
preflight and engine::withhold_everywhere; keep this branch's user-patch
gate and drop the preflight it sat beside. In CLI_CONTRACT.md keep the
branch's npm-family user-patch prose and table row, and take the base's
Eight-cases sentence (with NuGet), Go workspaces case, and the nuget and
sbt table rows.

Co-Authored-By: Claude <noreply@anthropic.com>
…se-attestation

# Conflicts:
#	crates/socket-patch-cli/CLI_CONTRACT.md
#	crates/socket-patch-cli/src/commands/vex_sources.rs
#	crates/socket-patch-core/src/vex/discover/mod.rs
…pnpm-yarn-vlt

Resolve the CLI_CONTRACT.md conflict: keep this branch's broader
vex_npm_patched_dependency row (pnpm, Yarn and patch-package patches)
and add the base's new vex_sbt_resolution_unverified and
vex_sbt_vendored_tree_missing rows from main (#2008).

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merged base fix/npm-discovery-false-attestation (which carries main c2be854) into this branch to fix the merge conflict. New head 85e98175. The only conflict was the vex_npm_patched_dependency row in CLI_CONTRACT.md: kept this branch's broader wording (pnpm, Yarn and patch-package patches) and added the base's new vex_sbt_resolution_unverified / vex_sbt_vendored_tree_missing rows. cargo check --all-targets (core + cli) and cargo test -p socket-patch-core --lib vex (793 passed) are clean locally. Draft stays stacked on #2010.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

The disk scan supplements the consent gate's walk with the Rush common
lock and a root shrinkwrap.yaml, but never read
common/config/subspaces/*/pnpm-lock.yaml. A patchedDependencies map or
patch_hash living only in a subspace lock was withheld by the in-memory
engine (those locks are candidate files there) but still pinned on
disk. The supplement now lists Rush locks through rush_lock_rels, the
same safe listing lockfile discovery uses.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Base automatically changed from fix/npm-discovery-false-attestation to main November 8, 2026 23:56
The old base fix/npm-discovery-false-attestation landed on main as the
squash 32e1630 (#2010), whose tree is identical to that branch's tip
f665796, which this branch already contains. Keeping this branch's tree
therefore applies exactly the PR's own changes on top of main.

Co-Authored-By: Claude <noreply@anthropic.com>
Final-review fixes for the user-patch gate:

- read_view treated a project file the view refused (a symlink whose
  target leaves the repository) as absent, because the refusal carries
  the NotFound kind. pnpm and yarn still read that file, so its
  patchedDependencies were missed and the package could be pinned.
  Check is_refused_read first and record unknown metadata, which
  protects every npm package, as the module promises.
- The in-memory selector fetched every nested package.json, pnpm/yarn
  lock and *.patch file for the user-patch walk even when npm was not
  among the selected ecosystems. A Python repo with debian/patches or
  many fixture manifests could then hit max_files. Select them only
  when npm is selected.
- The disk flow's Rush/shrinkwrap supplement dropped the
  user_patch_conflict warnings that withhold_user_patched_by returns,
  so a package withheld only by that evidence got a skip row but no
  warning, unlike the consent gate and the in-memory engine
  (CLI_CONTRACT promises both). Merge them into the run's warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Nov 9, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review (head 891cf58 → pushed c173014)

I reviewed the full diff and found three should-fix problems. All three are fixed in c173014:

  1. Fail-open on a symlinked file outside the repository (user_patches.rs, read_view). The view refuses a file whose real path leaves the repository, and that refusal carries the NotFound kind. read_view therefore treated a symlinked pnpm-workspace.yaml, package.json or lock that points outside the repo as absent, so its patchedDependencies were missed. main read through the link. The check now looks for is_refused_read first and records unknown metadata, which protects every npm package. New test: a_symlinked_file_outside_the_repository_fails_closed. It fails without the fix.
  2. The in-memory selector fetched user-patch files even when npm was not selected (hosted/memory/select.rs). It fetched every nested package.json, pnpm/yarn lock and *.patch with ecosystems: ["pypi"], which counts against max_files. A repo with debian/patches or many fixture manifests could fail a run that used to pass. These files are now selected only when npm is selected. New test: user_patch_files_are_not_selected_without_npm.
  3. Disk supplement warnings were dropped (scan/hosted.rs). The return value of withhold_user_patched_by was discarded with let _. A package withheld only by Rush or shrinkwrap.yaml evidence got a skip row but no user_patch_conflict warning, which goes against CLI_CONTRACT.md. Those warnings are now merged into the run's warnings.

Checks I ran: cargo clippy --locked --workspace --all-features -D warnings is clean. socket-patch-core lib: 7282 passed. The 4 failures are permission tests that also fail without this change when run as root. hosted_memory_engine, hosted_memory_parity and in_process_redirect_pnpm all pass.

Not fixed (follow-ups, not blocking):

  • No end-to-end disk test covers the Rush/shrinkwrap supplement. Deleting the if !common.allow_patch_conflicts block, or inverting it, would not fail any test.
  • Explicit get --mode hosted silently skips a package patched only in a Rush lock, where it should refuse with user_patch_conflict. The re-check under the lock uses the consent gate's index, which does not read Rush locks.
  • patch-package --cwd/--prefix before patch-package in a script is not treated like cd, and patch directories are not listed recursively (the recursive listing was already missing on main).

I removed Ready for review. The burn-down re-labels it once CI is green, and then I'll post the review brief.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

c173014 made a project file the view refuses (a symlink whose target
leaves the repository) record unknown metadata, which withholds every
npm package. A symlinked package-lock.json with no `patched` entry then
skipped the package with user_patch_conflict and exit 0, ahead of the
writer's redirect_symlinked_file_unsupported refusal, and broke the
symlinked-lock contract tests in scan/hosted_symlinked_files and
vendor/vendor_failure_paths (coverage (1) and (2)).

Read such a file through the link straight from disk instead, as main
did and as the walk already does for an ancestor file the bound admits.
Its declarations still protect their packages (no fail-open), a file
declaring nothing no longer withholds everything, and a file that can't
be read through the link either still fails closed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aeRbEUCCuLEgaocvKwD
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down: pushed 092feb1 to fix the 3 real coverage (1)/coverage (2) failures on c173014 (hosted_refuses_symlinked_package_lock_before_ledger_write, refused_commit_keeps_an_artifact_the_ledger_already_names, in_sync_vendor_over_a_symlinked_lock_neither_warns_nor_refuses).

Cause: c173014 recorded unknown metadata for a file the view refuses (a symlink escaping the repo), so a symlinked package-lock.json with no patched entry withheld every npm package with user_patch_conflict (exit 0) ahead of the redirect_symlinked_file_unsupported refusal.

Fix: read_view now reads such a file through the link straight from disk (as main did, and as the walk already does for admitted ancestor files). Declared patches still protect their packages, so this does not fail open. A file that can't be read through the link either still fails closed. The unit test is now a_symlinked_file_outside_the_repository_is_read_through_the_link, and an_unreadable_symlinked_file_outside_the_repository_fails_closed is new.

Local: user_patches unit tests 32/32, --test scan 224/224, --test vendor 133/133.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 092feb1. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Nov 9, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 092feb1d74a660552b7bbe23517fd4470a4ae496.

  • CI: 31/31 checks pass (19 skipped). No main-wide failures.
  • Bugbot: reviewed 092feb1 with no new issues. All review threads are resolved.
  • What changed since c173014: 092feb1 fixes the 3 symlinked-lock contract tests that failed in coverage (1) and coverage (2). A project file that symlinks outside the repo is now read through the link, not treated as unknown metadata, so a symlinked package-lock.json gets the expected redirect_symlinked_file_unsupported refusal again. Declared patches in such a file still protect their packages.
  • Worth a look: the read-through in read_view (user_patches.rs). It follows main's pre-PR behavior for symlinked files, not the final reviewer's "protect every npm package" rule. A file that can't be read through the link still fails closed.
  • Slack: not announced, because Slack posting is not available in this run. The next run should retry.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review at 092feb1: needs a human decision before this can go to the merge queue.

Detection fails closed in every path I traced, and cargo test -p socket-patch-core --lib user_patch passes (49/49). Two points need an owner's call, not a mechanical fix:

  1. Hosted in-memory path selection may push large repos over max_files. With the default SelectOptions.ecosystems = None, select.rs:474-477 turns user_patches on for every repo, including repos with no npm root. user_patch_need then selects:

    • every nested package.json, pnpm-lock.yaml and yarn.lock with content, member or not;
    • every *.patch file by name, before the WALK_SKIP check, so files under node_modules, .venv and target too.

    Each selected file counts toward the hard max_files limit (2000; limits.rs:212-219). Going over it fails the whole session.

    • Scenario: a monorepo with a few hundred examples/*/package.json + yarn.lock, or a Python/Go repo with debian/patches, that selected ~1.8k files before. It now errors.
    • Option A: keep the over-fetch, since failing closed is worth it.
    • Option B: select only when an admitted root is an npm root, keep .patch files out of WALK_SKIP dirs unless a --patch-dir could name them, and fetch non-member manifests only when a workspace glob could reach them.

    Which do you want?

  2. 092feb1 reads through symlinks that leave the repository. user_patches.rs:424-430 now reads a symlinked project file whose target is outside the repo straight from disk, as main did. That fixed the symlinked-lock contract tests. But CLI_CONTRACT.md:208 still says "Nothing outside the git repository root … is read". list_patch_directory (user_patches.rs:486) already lists a symlinked --patch-dir outside the repo without a bound.

    • Security impact is low: only declaration names reach output, and the writers refuse symlinked targets.
    • Do you want a carve-out in the contract for symlinks inside the repo, or should these fail closed, with the symlinked-lock tests expecting user_patch_conflict instead?

Smaller follow-ups, not blocking:

  • CLI_CONTRACT.md:226 says *.patch files are selected outside node_modules/.venv/…, but the code (and the .venv/patches test) selects them everywhere.
  • UserPatchIndex::unreadable_lock (user_patches.rs:131-139) treats a bound-refused read (NotFound kind) as a missing lock. The Rush writer's symlink refusal covers it today. Checking is_refused_read first would be cleaner.
  • list_patch_directory uses view.list_dir, not list_dir_strict, so non-UTF-8 names and mid-listing errors are skipped. Main recorded unknown there.
  • The Rush/shrinkwrap supplement isn't re-checked under the write lock (scan/hosted.rs:961-977). The race is narrow.

Labelled agent:needs-human. Once the two questions are answered, the burn-down or I can make the change.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] Review findings (Claude Opus + codex, independent passes):

  1. patch-package evidence is exact-version only (crates/socket-patch-core/src/patch/user_patches.rs:954, Opus; same as the open thread at :950). patch-package applies patches/<name>+<ver>.patch to whatever version is installed (it only warns on mismatch). Input: patches/left-pad+1.3.0.patch, lock bumped to left-pad@1.3.1, Socket patch for 1.3.1 → conflicts() is empty, the in-memory hosted engine pins the tarball and the new vex::discover::user_patched pass leaves the ref attestable; at install the user's diff stacks on Socket bytes or fails. Suggested fix: record patch-package evidence with version: None.
  2. Symlinked manifest hides --patch-dir (user_patches.rs:447, codex). A package.json symlinked to a manifest running patch-package --patch-dir fixes plus fixes/left-pad+1.3.0.patch still permits pinning left-pad@1.3.0 without consent.
  3. Rush evidence read outside the apply lock (crates/socket-patch-cli/src/commands/scan/hosted.rs:888, codex). Rush patchedDependencies is read before acquiring the lock and not rechecked, so a patch added while scan --mode hosted --lock-timeout 30 waits gets pinned without consent.

Minor: list_patch_directory ignores scoped patches/@scope/name+1.0.0.patch subdirectories; the new test in hosted/engine.rs:4827 sits under the #713 HEAL-ON-RERUN doc comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] Approving per maintainer decision; see the findings in the review above.

…heck

- patch-package evidence now protects every version of the named package:
  patch-package applies `<name>+<version>.patch` to whatever version is
  installed and only warns on a mismatch. Patch directories are walked
  recursively (following linked subdirectories, skipping the walk's
  skip list), so `@scope/name+1.0.0.patch` under a `@scope/` directory
  counts, as patch-package's klaw-sync collection reads it. A directory a
  script names is listed strictly; the default `patches/` as before.
- An in-memory `package.json` that is a link fails closed (its
  `--patch-dir` scripts and declarations are unknown, and the engine does
  not refuse a linked manifest), except beside a yarn berry lock, where
  the engine's own symlink refusal stands.
- The hosted disk flow re-reads the Rush / root `shrinkwrap.yaml`
  supplement under the apply lock, so a patch recorded while the run
  waited still withholds its package. The supplement's lock reads move
  into core (`engine::withhold_user_patched_by_supplement`), sharing one
  reader with the in-memory engine that reads a lock linking out of the
  repository through the link, as the walk does.
- CLI_CONTRACT: carve-out for symlinked project files read through their
  link (owner decision), the `*.patch` selection wording, and the
  patch-package every-version rule.
- Move the #713 doc comment back onto its own test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Review follow-up: pushed 3ab7c1993. It addresses the review threads, Wenxin's review and the two owner decisions from the 11:59Z comment.

Owner decisions

  1. max_files over-fetch: Option A. Keep the over-fetch and fail closed. The CLI_CONTRACT wording now matches the code: manifests and locks are selected outside the skip list, and every *.patch file is selected wherever it is.
  2. Symlinks: contract carve-out. A project file inside the repository that is a symbolic link is read through its link. If it can't be read that way, it protects every npm package.

Wenxin's review

  1. patch-package exact version: fixed. Evidence now protects every version, and patch directories are walked recursively (@scope/name+1.0.0.patch). See the resolved thread.
  2. Symlinked manifest hides --patch-dir: fixed. On disk the link was already read through; a test now pins that. In memory, a linked package.json has no content and the engine doesn't refuse it, so it now fails closed. The one exception is a root manifest beside a yarn berry lock: the engine refuses that one as a rewrite target, so its refusal stands. Test: an_in_memory_linked_manifest_fails_closed.
  3. Rush evidence read outside the apply lock: fixed. The Rush / root shrinkwrap.yaml supplement is re-read under the apply lock, so a patch recorded while the run waited still withholds its package. The supplement's lock reads moved into core (engine::withhold_user_patched_by_supplement) and share one reader with the in-memory engine. That reader reads a lock that links out of the repository through the link. New disk-flow test: scan_redirect_withholds_a_package_a_rush_lock_records_as_patched. I checked that it fails when the supplement is disabled.
  4. Minor:

Smaller follow-ups from the 11:59Z comment

  • unreadable_lock now checks is_refused_read first.
  • A --patch-dir named by a script is listed with list_dir_strict. The default patches/ keeps the lenient listing.

/code-review high on the delta: 8 findings. Six are fixed in the same commit:

  • a linked berry manifest;
  • two lock read-through cases;
  • linked patch subdirectories;
  • --patch-dir . listing node_modules;
  • strict listing of the default patches/;
  • the duplicated supplement reader.

Two were declined:

  • The late Rush re-read runs after the rollout may_write decision. The effect is that a run whose only writable candidate gets withheld under the lock still holds the lock while writing nothing. Rollout admission still reads the remaining candidates, so nothing is mis-admitted.
  • Recursion into a huge patch tree hits the 10k-entry cap and fails closed. That is intended.

Tests:

  • core lib (user_patches, vex::discover, hosted, patch::redirect, repo_root): 1610 passed.
  • CLI user_patches 7, hosted_memory_engine 50, hosted_memory_parity 43, in_process_redirect_pnpm 42, in_process_redirect 136, vex 28, e2e_vex_lockfile 322, scan 224, vendor 133, lib 867.
  • clippy -D warnings and fmt are clean.

Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 6d17873 Nov 9, 2026
49 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/user-patched-deps-pnpm-yarn-vlt branch November 9, 2026 22:11
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 9, 2026
Brings #2018 (never patch a user-patched pnpm/yarn/vlt dependency).
Conflict in CLI_CONTRACT.md "Unattested references": kept main's
patchedDependencies wording and its new pnpm/Yarn/patch-package
paragraph, and this branch's two Cargo cases (count Eight -> Ten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 9, 2026
#2018's user_patched extractor built UnwiredCopy marks with the old
file-only scope; #2009 scopes them by (file, TreePart). Each mark is
for exactly one ref, so it takes that ref's own (source_file, part), as
npm::mark_user_patched does. A pnpm env-lockfile ref of a user-patched
package is marked too: the index is per package, so a missed
attestation is the safe side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants