Repository navigation
Never patch a pnpm, yarn or vlt dependency the user patched - #2018
Conversation
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>
bbd39aa to
9677ca9
Compare
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>
9677ca9 to
3f076f6
Compare
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>
|
Burn-down agent: merged base Generated by Claude Code |
|
bugbot run Generated by Claude Code |
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>
|
bugbot run Generated by Claude Code |
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>
|
Final review (head 891cf58 → pushed c173014) I reviewed the full diff and found three should-fix problems. All three are fixed in c173014:
Checks I ran: Not fixed (follow-ups, not blocking):
I removed Generated by Claude Code |
|
bugbot run Generated by Claude Code |
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
|
Burn-down: pushed Cause: c173014 recorded unknown metadata for a file the view refuses (a symlink escaping the repo), so a symlinked Fix: Local: Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ 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.
|
Ready for review at
Generated by Claude Code |
|
Final review at Detection fails closed in every path I traced, and
Smaller follow-ups, not blocking:
Labelled Generated by Claude Code |
Wenxin Jiang (Wenxin-Jiang)
left a comment
There was a problem hiding this comment.
[agent] Review findings (Claude Opus + codex, independent passes):
- 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 appliespatches/<name>+<ver>.patchto whatever version is installed (it only warns on mismatch). Input:patches/left-pad+1.3.0.patch, lock bumped toleft-pad@1.3.1, Socket patch for 1.3.1 →conflicts()is empty, the in-memory hosted engine pins the tarball and the newvex::discover::user_patchedpass leaves the ref attestable; at install the user's diff stacks on Socket bytes or fails. Suggested fix: record patch-package evidence withversion: None. - Symlinked manifest hides
--patch-dir(user_patches.rs:447, codex). Apackage.jsonsymlinked to a manifest runningpatch-package --patch-dir fixesplusfixes/left-pad+1.3.0.patchstill permits pinningleft-pad@1.3.0without consent. - Rush evidence read outside the apply lock (
crates/socket-patch-cli/src/commands/scan/hosted.rs:888, codex). RushpatchedDependenciesis read before acquiring the lock and not rechecked, so a patch added whilescan --mode hosted --lock-timeout 30waits 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.
Wenxin Jiang (Wenxin-Jiang)
left a comment
There was a problem hiding this comment.
[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>
|
Review follow-up: pushed Owner decisions
Wenxin's review
Smaller follow-ups from the 11:59Z comment
Two were declined:
Tests:
Generated by Claude Code |
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>
#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>
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 pnpmpatchedDependencies, yarn berrypatch:and patch-package for every npm-family project. Unless--allow-patch-conflictsis passed, it keepsscan(all modes),get,vendor,applyand the hosted/vendored writers from wiring those packages. Two consumers did not use that evidence:--allow-patch-conflictsrun wrote.hosted-bundle) has no consent gate in front of it, so it rewired those packages.One detector, three consumers
UserPatchIndexhas a single walk over aProjectView(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.patchedDependencieskeys (name,name@version,name@range)package.jsonpnpm.patchedDependencies,pnpm-workspace.yamlpatchedDependencies, each pnpm lock's top-levelpatchedDependenciesmap and(patch_hash=…)key suffixes (root, Rush, members, and a ref's own lock)patch:descriptors (Yarn'sbuiltin<compat/…>patches are excluded), every layer peeled, aliases resolvedpackage.jsondependency maps andresolutions;patch:locators thatyarn.lockresolves, including one that wraps the hosted tarball (#962)patches/<name>+<version>.patch, or a--patch-dirthat apackage.jsonscript namesThe 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-dirbeyond that root is not read. Instead it protects every npm package with an "outside the git repository" diagnostic, in the gate too.--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.ReadScope { Workspace, Global }, chosen once from the CLI flags viaGlobalArgs::read_scope()and passed throughUserPatchIndex::discover_scoped. It has the same shape as Never read or rewrite project files outside the git repository root #2025'sutils::repo_root::ReadScope(same variants,of_global) and is meant to be replaced by that type when the two branches meet.The npm-family scope leaves out npm's and Bun's own top-level
patchedDependenciesand npm's lock-entrypatchedrecords. 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 nopatchedDependencies, nopatch_hashand nopatchcommand. Itsvlt.jsonmodifiersrewrite 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
vex::discover::user_patchedpass marks every npm ref the index protects asUnattestedKind::UserPatchedDependency. Like Stop attesting npm-family pins pnpm or a user patch overrides #2010's npm and Bun marks, each mark is anUnwiredCopywithCopyTarget::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, sorollback,removeandliststill find it.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 patchmade on a hosted or vendored package is silently dropped by the next superseding re-run, takeover, rollback or revert (the #367 guard only matchesname@versionkeys) #1019) are now marked too.npm::mark_user_patched(viauser_patch_source). It now reads keys against no target, so any key for thatname@versioncounts, and that case gets its own detail.redirect_bun_patched_dependency_skipped. Either way vex's plan omits the pin through the mark.engine::withhold_user_patchedremoves such candidates from every rewriter and records each as askippedrow with reasonuser_patch_conflict(the consent gate's code). In memory it also adds a warning.--allow-patch-conflictsis passed. There it mostly duplicates the consent gate, but it also reads the Rush lock, which the gate does not.vendorand the vendored writers refuse withuser_patch_conflict.scan --mode vendoredandapplyskip 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.CLI_CONTRACT.md (Existing project patches, Unattested references, the
vex_npm_patched_dependencyrow) and docs/usage.md are updated.Tests
Verified to fail before:
vex::discover::user_patched::tests:package.jsonpnpmfield, lock map,patch_hashkey, range and bare keys.package.jsonpatch:wrapping the hosted URL; ayarn.lockpatch locator.--patch-dir.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:--allow-patch-conflicts: pinned;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 newthe_repository_bound_holds_for_workspace_runs_onlytest: 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 includescommitted_fixture_corpus_matches_golden, with no golden changes.hosted_inventory21,pnpm_hosted6,vlt_locks10,upstream_restore_golden53.in_process_redirect_pnpm42,in_process_redirect152,hosted_memory_engine50,hosted_memory_parity43,user_patches27,vex48,e2e_vex_redirect53, and libvex_sources/user_patches/scan::hosted63. All pass.cargo clippy --locked --workspace --all-features -- -D warningsandcargo fmt --all -- --checkare clean.Review findings (
/code-review high)All ten findings were confirmed. Each is fixed in
93709da82.projectRoots: [""]), a pnpm member with its own lock is not a root of its own. Itspackage.jsonwas 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.--patch-dirthat has no listing in memory. Selection supplies every*.patch, so such a directory holds none.--patch-dircontents never selected: confirmed and fixed. Selection now requests every*.patchfile by name (presence-only is enough, since only the file names are read).patches/left-pad+1.3.0.patch→ memory pinnedleft-pad. Selection now fetches everypackage.json,pnpm-workspace.yaml,pnpm-lock.yamlandyarn.lock, and every*.patchby name, outsidenode_modules,.git,.socket,targetand.venv. That list is a constant (WALK_SKIP) shared with the walk.hosted_memory_parity::user_patches_are_selected_and_agree_with_the_disk_flowdrivesselect_paths→ memory project → engine for 8 cases (pnpm member manifest, npm memberpatches/, npm memberpatch:descriptor, rootpatches/, root--patch-dir, and three no-patch controls). Each case runs with both detected and named roots and is compared with the disk flow.go.workmember recorded under npm: confirmed and fixed. Every pending directory now carries the ecosystem whose declaration named it. An out-of-repogo.workusemember protects Go modules (fail closed for Go), not npm packages. Test:an_out_of_repo_go_member_protects_go_not_npm...: confirmed and fixed. Ancestors now come fromutils::repo_root::search_dirsand are checked and read at the same canonical absolute path. Test:a_symlinked_root_reads_its_real_ancestors.an_absolute_patch_dir_inside_the_repository_is_read.UserPatchIndex::from_locks+engine::withhold_user_patched_by). The consent gate has already walked the project.npm_family_inskips a passedpnpm-lock.yamloryarn.lockwhose directory the walk already read.vlt::artifact_preflight. The in-memory preflight is offline and downloads nothing.lexical_join: confirmed and fixed. They are replaced byfind_git_repo/search_dirs(which honorGIT_CEILING_DIRECTORIESand the home rule) andnormalize_lexically_keeping_escapes. The minimalReadScopestays, to be replaced by Never read or rewrite project files outside the git repository root #2025's.Two caveats on the tests:
package.jsonand lock adds fetches for large monorepos. That is the cost of reading members' declarations at all. The host'smax_fileslimit still applies and fails the run closed.Not done
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 highon93709da)shrinkwrap.yamlrecords 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.repo_root::search_dirsstops only exactly at$HOME. A project outside home with no checkout therefore read/package.jsonand 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..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
HOMEoverride, which isn't safe in parallel lib tests. Findings 1 and 3 need a disk-flow CLI fixture. The existinguser_patchesCLI suite (27) and the core suites pass.Declined or deferred:
seendedup by path drops the npm tag of a dir also queued as a go.work member. This is a corner case. Defer to the Never read or rewrite project files outside the git repository root #2025ReadScopeswap, which rewrites this bound.WALK_SKIP-segment and root-level patch dirs (--patch-dir target/patches,--patch-dir .). This is real but narrow. The disk flow (the default) withholds correctly. Follow-up.--patch-dirvs the canonical repo root. Never read or rewrite project files outside the git repository root #2025'sReadBoundaryreplaces this lexical check.Tests (cloud run):
cargo test -p socket-patch-core --lib -- user_patch vex::discover:: hosted:: utils::repo_rootgives 687 passed. CLI--test user_patchesgives 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, Yarnpatch:descriptors, patch-package) now follow the same rules as npm/BunpatchedDependencies: by default hosted scan skips them asuser_patch_conflict, vendored/getstill refuse without consent, andvexnever attests them while the user patch is declared—even after an--allow-patch-conflictsrun wires the pin.UserPatchIndexis refactored into one shared walk (discover_scoped/npm_family_in) withReadScope::WorkspacevsGlobaland 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 applieswithhold_user_patched(no consent knob); disk scan supplements the consent gate with Rush/subspace and rootshrinkwrap.yamllocks. Path selection now fetches nested manifests, locks, and*.patchfiles the walk needs so memory and disk stay aligned.Documentation and user-facing notes broaden
vex_npm_patched_dependencyto 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 highfindings not fixed in this PRFixed 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 inCLI_CONTRACT.md. Left for follow-ups, with reasons:shrinkwrap.yamlevidence lives only in the hosted disk-scan supplement, not the sharedUserPatchIndex::discovergate used byvendor/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_needin hosted memory selection streams every nestedpackage.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).patchedDependencieskey 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 highon the delta. The details are in the follow-up comment.@scope/name+1.0.0.patchcounts. The walk follows linked subdirectories and skipsnode_modules/.git/etc.package.json: fails closed. The exception is the root manifest beside a yarn berry lock, which the engine refuses as a link.shrinkwrap.yamlsupplement: 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.*.patchselection wording and the every-version rule are updated.Generated by Claude Code