This is the detailed support matrix for socket-patch: which package ecosystems work
with which patch mode, the per-ecosystem caveats, and
the platforms the binary ships for.
For what the three modes are and how to choose between them, see Patch modes in the README.
The backticked slug in each row is the value -e/--ecosystems accepts (e.g.
--ecosystems npm,pypi,golang).
| Ecosystem | agent (--mode agent) |
vendored (--mode vendored) |
hosted (--mode hosted) |
|---|---|---|---|
npm (npm) — pnpm / yarn / berry / bun / vlt |
✅ any install layout, vlt's node_modules/.vlt store included (every store copy, copy-on-write) |
✅ seven lockfile flavors: package-lock, yarn classic, yarn berry (node-modules linker; PnP refused), pnpm v9, pnpm legacy v5.4/v6.0 (pnpm 7/8 — frozen installs are path-bound because those majors absolutize file: override specifiers; moved checkouts run one pnpm install --offline --no-frozen-lockfile, surfaced as vendor_pnpm_legacy_absolute_specifier), bun text bun.lock lockfileVersion 0/1/2 and native binary bun.lockb revisions 1/2/3 (binary locks stay binary; text workspace vendoring requires lockfileVersion 2 — see Bun compatibility), vlt vlt-lock.json lockfileVersion 0/1 (patched package directories for direct dependencies of the root or a workspace member; transitive targets refused — see vlt notes). Rush monorepos refused (vendor_rush_unsupported) — see Rush notes |
✅ package-lock / npm-shrinkwrap, pnpm-lock.yaml and legacy shrinkwrap.yaml (pnpm majors 1–12; block and flow resolutions), yarn classic, yarn berry, bun, vlt (vlt-lock.json without lockfileVersion, 0 or 1) — pnpm, berry, bun and vlt carry constraints, see npm hosted-mode notes and vlt notes |
PyPI (pypi) — uv / poetry / pdm / pipenv / pip |
✅ in place | ✅ uv project/script locks, PEP 751 pylock.toml / pylock.<name>.toml, poetry, pdm, pipenv (Pipenv 2018 or later — every Pipfile.lock category is rewired, lock-only checkouts included; Pipenv 2023+ does not hash-check local wheels — vendor_integrity_unverified; a venv still holding the upstream release is reported as pypi_pipenv_stale_install; see Pipenv compatibility), and requirements.txt. Native uv vendoring requires uv ≥ 0.2.35 (the [[package]] lock grammar); hosted mode covers native uv.lock from uv 0.1.45 (the first release whose uv lock writes one) and requirements from uv 0.0.5; see uv compatibility. |
✅ requirements.txt including hash continuations, uv project/script locks, and PEP 751 locks. Version/source ambiguity is refused; see uv compatibility. Poetry 1.x and 2.x locks are supported; Poetry 0.x ignores URL sources and is refused. See Poetry compatibility. Pipenv Pipfile.lock (pipfile-spec 6 — Pipenv 7 and later; path references for 7–11, file from 2018; lock-only checkouts and Pipenv's out-of-tree venv are discovered; a warm venv that Pipenv will not reinstall over warns redirect_pypi_stale_install; see Pipenv compatibility). pdm.lock is supported for the lock formats PDM 0.12–1.4 and 2.8.1+ write (lock_version 2 / 4.3–4.5.1); the identity-losing 3.1 / 4.0–4.2 formats (PDM 1.8–2.7) are refused. PDM 2.8.0 writes an indistinguishable 4.3 lock but shares that identity-loss bug, so a rewritten 2.8.0 lock crashes pdm sync — upgrade to ≥ 2.8.1. See PDM compatibility. |
Cargo (cargo) |
✅ in-place + .cargo-checksum.json rewrite (shared registry-cache caveat — see Cargo: shared registry cache) |
✅ [patch.crates-io] path entry in the root Cargo.toml (v5; per-version Socket keys; pre-v5 .cargo/config* wiring migrates on re-run) |
✅ per-patch sparse registry ([registries.socket-patch-<uuid>] + Cargo.lock source/checksum); direct dependencies only — a crate another dependency also pulls in is refused, use --mode vendored; with no Cargo.lock the graph is unknown, so only a project whose sole dependency is the patched crate is redirected |
RubyGems (gem) |
✅ in place | ✅ Gemfile + Gemfile.lock path pair (Gemfile spelling only — a gems.rb twin, which bundler ≥ 2 loads instead, or a BUNDLE_GEMFILE-configured manifest makes vendoring refuse with gemfile_not_loaded before any write) |
✅ per-dep source block — edits gems.rb + gems.locked or Gemfile + Gemfile.lock, whichever the project holds (a Gemfile + gems.rb twin is refused with redirect_gem_twin_manifest_ambiguous: bundler 1.x loads the Gemfile, bundler ≥ 2 loads gems.rb, and nothing in the project says which bundler installs it; bundler 4's BUNDLE_LOCKFILE (environment, app config or global config) naming any other lock is refused with redirect_gem_bundle_lockfile_unsupported, as is vendoring with gemfile_not_loaded; spellings that diverge beyond Socket's own edits fail closed with redirect_gem_gemfile_spellings_diverge; BUNDLE_GEMFILE from .bundle/config (which outranks the environment, as in bundler), the environment, or the global ~/.bundle/config / $BUNDLE_USER_CONFIG (lowest, as in bundler) is followed when it names the project's Gemfile / gems.rb, and any other configured manifest is refused with redirect_gem_bundle_gemfile_unsupported); a Bundler all-source, exact-source or hostname mirror in app config or the scan environment can capture the patch registry, so the redirect is refused with redirect_gem_mirror_overrides_source without printing mirror URLs (scope mirrors to mirror.https://rubygems.org; user-global config and mirrors set only in a later install environment are not inspected); the CHECKSUMS pin needs bundler ≥ 2.6 (older locks get a redirect_gem_no_checksums_section warning); a stale pre-redirect materialization that bundle install would reuse instead of refetching is flagged redirect_gem_stale_install with a prescriptive remedy (see CLI_CONTRACT.md's "Gem stale-install guard") |
Go (golang) |
✅ go.mod replace → .socket/go-patches/ — see Go: directory replaces and go.sum |
✅ replace → the committed vendor tree |
✅ (free tier) fork-style replace → patch.socket.dev/gopatch/<uuid> + committed go.sum pin; see Go notes. Paid hosted patches are unsupported; redirect_golang_unsupported names the vendored remedy |
Maven (maven) — Maven and Gradle |
✅ in place in every copy the build consumes: each ~/.m2 copy it reads and each Gradle files-2.1 copy; ~/.m2 .sha1/.md5 sidecars are rewritten, Gradle copies get advisories; jar-member records swap in the patch service's whole jar — prefer vendored / hosted, see Maven & NuGet caveats and Gradle |
✅ suffixed Maven repository (<version>-socket.<hex8> pin + .mvn/maven.config + fallback file repository) for every pom root, single-module or reactor, or Gradle 6.8+ same-GAV repository with settings wiring and SHA-256 checks (a root with both pom.xml and a Gradle build wires both); see JVM vendoring and Gradle |
✅ fail-closed by a Socket-only <version>-socket.<hex8> suffix: pom projects get a pinned <version> (${property} versions are refused); Gradle 6.8+ builds get an owned settings script, lock-entry rewrites and a resolution tripwire — see Maven & NuGet caveats and Gradle |
sbt / Mill / scala-cli (maven) |
✅ Coursier caches (sbt 1.3+, sbt 2, Mill, scala-cli) and Ivy caches (sbt 0.13–1.2, useCoursier := false) patched in place, Coursier checksum sidecars resynced — see Scala build tools |
✅ sbt 0.13.18+: generated socket-patch-vendor.sbt over the committed suffixed .socket/vendor/maven2 tree; scala-cli directory builds: owned socket-patch.scala + same-GAV .socket/vendor/coursier tree (Linux / macOS); Mill: not wired (agent or hosted guidance) |
✅ sbt 0.13.18+: one generated socket-patch.sbt, gated on sbt's own sbt update records; Mill / scala-cli: paste-able snippets only (redirect_mill_manual_snippet, redirect_scala_cli_manual_snippet) |
NuGet (nuget) |
✅ in-place patching deletes .nupkg.metadata and advises on the .nupkg.sha512 tamper-evidence sidecar — prefer vendored / hosted, see Maven & NuGet caveats |
✅ committed folder feed + packageSourceMapping + packages.lock.json contentHash pin |
✅ nuget.config source + source-mapping, packages.lock.json contentHash rewrite. See the locked-mode note in Maven & NuGet caveats |
Composer (composer) |
✅ in place (vendor/) |
✅ composer.lock dist: path rewrite |
✅ composer.lock dist url + shasum rewrite; the entry's source and dist.mirrors are removed. See composer-compatibility.md |
Deno (deno) |
✅ in place (the only mode for Deno) | ❌ refused (vendor_unsupported_ecosystem) |
❌ not supported |
Maven / NuGet sidecar caveat: Maven and NuGet are fully enabled in every mode. In-place (agent-mode) patching writes into shared caches: NuGet's post-apply fixup deletes
.nupkg.metadataand raises an advisory for the signed-package.nupkg.sha512tamper marker it cannot honestly rewrite. A~/.m2copy's.jar.sha1/.jar.md5that described the pre-patch bytes are rewritten to the patched bytes (and back on rollback); one that already disagreed is left alone. A Gradlefiles-2.1copy has no checksum file (its directory names the download's sha1), so each patched copy gets advisories instead:gradle_refresh_reverts,gradle_daemon_staleandgradle_global_cache_shared(see Gradle). The copy-out modes —vendor,scan --mode vendored,scan --mode hosted— never write into the caches and avoid the issue entirely.
The matrix above lists what each mode handles. This table says how much that support is worth for v5.x. v5 removes no format or mode: everything in the matrix works in v5.0 and keeps working through v5.x.
| Tier | Meaning |
|---|---|
| Supported | The default. Covered by CI. A regression is a release blocker. |
| Beta | Works for the build shapes documented on this page and is tested in CI. Some shapes outside them are refused, and the open issues for that ecosystem describe shapes that are patched incompletely. Preview with --dry-run and confirm with a real build before you commit the result. |
| Legacy | A format that the package manager itself has retired. socket-patch keeps reading and writing it in v5 and still fixes its bugs, but adds no new features for it. Prefer the upgrade path in the table below. |
| Not supported | Refused with an error code that names the remedy. |
| Ecosystem / format | agent | vendored | hosted |
|---|---|---|---|
npm: package-lock / npm-shrinkwrap, yarn classic, yarn berry, pnpm lock v9, text bun.lock, vlt lock eras C–F |
Supported | Supported | Supported |
npm: binary bun.lockb |
Supported (agent mode patches node_modules, not the lock) |
Legacy | Legacy |
| npm: pnpm 7/8 locks (lockfileVersion 5.4 / 6.0) | Supported | Legacy | Supported |
npm: older pnpm locks (pnpm 1–6, shrinkwrap.yaml) |
Supported | Not supported | Supported (see npm hosted-mode notes for the version floors) |
| npm: vlt lock eras A0–B (before 1.0.0-rc.15) | Supported | Legacy (A0 is refused) | Legacy |
| PyPI, Cargo, RubyGems, Go, NuGet, Composer | Supported | Supported | Supported, with the per-ecosystem limits in the matrix |
Maven (pom.xml builds) |
Supported, with the sidecar caveats | Beta | Beta |
| Gradle | Supported, with the files-2.1 advisories |
Beta (Gradle 6.8+) | Beta (Gradle 6.8+) |
| sbt / Mill / scala-cli | Beta | Beta (sbt and scala-cli directory builds; Mill is not wired) | Beta (Mill and scala-cli get manual snippets only) |
| Deno | Supported | Not supported | Not supported |
Hosted and vendored Maven and Gradle are Beta in v5. Each wiring is built to fail
closed: hosted mode and vendored Maven pin a Socket-only -socket.<hex8> version, and
vendored Gradle checks the SHA-256 of the vendored artifact, so a build that cannot get
the patched artifact fails instead of silently building the upstream jar. But the pom
and Gradle rewriters still have open gaps. For example: multi-module builds, <classifier>
dependencies, dependencies declared inside profiles, plugins or comments, ${property}
coordinates, imported BOMs, and repository mirrors. The open
pm:maven
and pm:gradle
issues track them. Before you rely on a JVM --vex attestation, run the build and check
that it resolves the patched artifact of each patched dependency.
Each Legacy format has an upgrade path and an undo path. Both work in v5:
| Format | Leave the legacy format | Undo socket-patch's changes |
|---|---|---|
Binary bun.lockb |
Bun 1.2+ writes text bun.lock by default. Undo socket-patch's changes (next column), convert with bun install --save-text-lockfile --frozen-lockfile --lockfile-only, delete bun.lockb, then rerun socket-patch scan --mode <mode> with the mode you used before |
Vendored: socket-patch rollback. Hosted: rollback refuses a hosted bun.lockb pin, because the binary lock cannot be re-derived. Restore the lock from version control (git checkout -- bun.lockb), as the refusal says |
| pnpm 7/8 lock, vendored | Run socket-patch rollback, re-lock with pnpm 9 or later, then rerun socket-patch scan --mode vendored. A pnpm 9+ lock is not path-bound (see vendor_pnpm_legacy_absolute_specifier in the matrix) |
socket-patch rollback |
| vlt eras A0–B | Run socket-patch rollback, re-lock with vlt 1.0 or later, then rerun socket-patch scan --mode <mode> with the mode you used before |
socket-patch rollback |
- v5.x minor and patch releases do not remove a format or mode from the matrix, and do not make a format refuse that v5.0 accepts. Bug fixes can still refuse a specific input that would otherwise produce a wrong result. Each refusal has an error code and a remedy.
- A tier can move up (Beta to Supported) in any release. A move down, or a new Legacy entry, is announced in the release notes and on this page.
- Removing write support for a Legacy format can happen only in a major release.
The removed path becomes a refusal with an error code and a re-lock remedy. socket-patch
keeps reading the format for
list,vexandrollback, so state that an older socket-patch wrote can still be undone (or is refused with a version-control remedy, as hostedbun.lockbis today).
- npm (package-lock.json / npm-shrinkwrap.json) — every present npm lock is
rewritten (npm <= 11 installs from a committed shrinkwrap, npm 12 only from
package-lock.json). npm 12 never reads npm-shrinkwrap.json: on a
shrinkwrap-only project it resolves the tree from the registry and writes a
fresh package-lock.json, so the patch reaches npm <= 11 only. Hosted and
vendored runs still rewire the shrinkwrap but warn
(
redirect_npm_shrinkwrap_only/vendor_npm_shrinkwrap_only), andvexomits those patches (vex_npm_shrinkwrap_only) whilelist,rollbackandremovestill manage them; rename the lock to package-lock.json (or commit a copy under that name) and re-run. npm 12 defaultsallow-remote=noneand refuses the redirected tarballs (EALLOWREMOTE) unless the project.npmrcsetsallow-remote=all, so the hosted run writes it (new file, or one appended line; oncerollback/remove/ the vendored takeover has restored the last hosted lock entry, a file holding only that line is deleted, otherwise the line stays with annpm_allow_remote_leftwarning) and always warnsredirect_npm_allow_remotewith the tradeoff (any url-resolved dependency is then admitted; sha512 pins stay enforced). Commit.npmrcwith the lock. An explicit userallow-remote=none/rootis respected (never rewritten or overridden) — in the project.npmrc, the user / global / builtin npm config, or annpm_config_allow_remoteenvironment variable — and--no-npm-allow-remote-configopts out (install withnpm ci --allow-remote=all). Vendoredfile:tarballs are unaffected byallow-remote: npm >= 11.14 gates them byallow-file(defaultall). An explicitallow-file=none, orallow-file=rootwhile a vendored copy is transitive, makes every install fail EALLOWFILE; the vendored run keeps the setting, warnsvendor_npm_allow_filewith the remedy (allow-file=allin.npmrc, ornpm ci --allow-file=all), andvendor --checkfails. npm >= 8'sreplace-registry-hostset toalways(or to the hosted patch host) makes npm rewrite the hosted pins to the configured registry, so every install fails E404: the hosted run reads it from the same env / project / user / global / builtin layers and warnsredirect_npm_replace_registry_host(setreplace-registry-host=npmjsin the project.npmrc, or use vendored mode). npm 6 ignoresresolvedfor registry dependencies, so a redirected lockfileVersion 1 lock fails closed with EINTEGRITY under npm 6 (redirect_npm_legacy_client) and installs under npm= 7. A lockfileVersion 2 lock's legacy
dependenciesmirror is rewired withpackages, npm alias nodes ("lp": {"version": "npm:left-pad@1.3.0"}) included. npm 6 installs an aliased dependency from the configured registry whatever itsresolvedsays, so under npm 6 an aliased hosted pin in a v2 lock fails closed with EINTEGRITY too (redirect_npm_legacy_alias_client). Lockfile-onlyvexattests nothing for a package whosepackagesentry is wired while the v2 mirror still resolves it from the registry. A mirror node with noresolvedbut the patchedintegrity(what npm 7–12npm installleaves on a vendored v2 lock, since npm never writes afile:resolvedthere) still counts as wired forvexandvendor --check: npm 6 fails closed on that pin. Vendoring needs a lockfileVersion 2/3 lock (npm 6 still installs a vendored v2 lock from its legacy mirror, alias nodes included) and rewires both locks in npm 12's dual-lock state. Majors 6–12 are measured in npm compatibility. - pnpm — hosted rewriting supports legacy
shrinkwrap.yaml(pnpm 1/2), lockfileVersion 5.x (pnpm 3–7), 6.0 (pnpm 8), and 9.0 (pnpm 9–12). Early pnpm 1 locks with shrinkwrapVersion 3 and no positive minor version are refused because those installers discard hosted URLs; the tested pnpm 1 floor is 1.43.1. Upgrade and regenerate that lock, or use agent mode. Block and flow resolutions, scoped names, aliases, nested peer contexts, workspaces, nested Rush locks, and LF/CRLF line endings are handled. Every matching package instance is rewritten; an unsupported instance prevents confirming that dependency across the lockfile set. Rollback preserves the original resolution fragments. A workspace withsharedWorkspaceLockfile: false(shared-workspace-lockfile=falsein.npmrcon pnpm 10 and older) installs each member from its own lock: run from the workspace root, everypackages:member'spnpm-lock.yamlis pinned beside the root's (pnpm 7 writes no root lock at all), andlist,vexandrollbackread the member locks too. Member locks beside a root lock that lists member importers are stale and ignored. A member list the CLI cannot read (including apnpm-workspace.yamlwith nopackages:key) is refused withredirect_pnpm_member_locks_unresolved. WithgitBranchLockfileon (git-branch-lockfile=truein.npmrcon pnpm 10 and older), pnpm installs a branch from its ownpnpm-lock.<branch>.yaml(in each member's directory too, when members keep their own locks), which neither mode can pin: while such a lock exists, hosted mode refuses the pnpm pins withredirect_pnpm_git_branch_lockfileand vendored mode withvendor_pnpm_git_branch_lockfile. Turn the setting off and runpnpm install --merge-git-branch-lockfiles, then re-run. With no branch lock,pnpm-lock.yamlis the lock pnpm installs from and is pinned as usual. For 9.0 root or member locks, the CLI configurestrustLockfile: truein the rootpnpm-workspace.yamlunless opted out with--no-trust-lockfile-configor explicitly disabled by the project. pnpm >=11 needs this for hosted URLs. This skips registry re-verification for the whole lock; tarball integrity remains enforced. pnpm <=10 does not need the setting. A project with nopnpm-workspace.yamlthat pins pnpm 9.0–10.4 (packageManager,devEngines,engines.pnpm, or the pnpm that last installednode_modules) gets no file: there a root-only workspace makespnpm add <pkg>fail withERR_PNPM_ADDING_TO_ROOT. Re-run the scan after upgrading to pnpm >=11. When no pin says which pnpm runs, the file is created, and pnpm 9.0–10.4 then needpnpm add -w <pkg>. Vendored mode follows the same rule for itsoverrides:mirror. Reinstall after redirecting: a successful warm-cache install can retain upstream bytes. Use a clean install tree and an empty store;--forceis not a reliable substitute. Runsocket-patch vexafter installation to verify the patched files. See the compatibility matrix and workflow. - yarn classic — the
yarn.lockentry'sresolved/integrityare rewritten to the hosted tarball.resolvedalways carries the tarball's#<sha1>fragment: yarn 1 names its cache slot after it, so a fragmentless URL would share the slot of an unpatched copy of the same version and a warm cache would install those bytes (or fail the integrity check). For an entry it hasn't pinned yet (or a grant with no sha1), the scan downloads the served tarball and checks it against the grant's sha512. It pins the sha1 of those bytes when the grant has none, and it compares the tarball's ownpackage.jsonwith the lock: yarn 1 installs only the dependencies the lock names, so when the patch adds a dependency (or changes a range) that noyarn.lockblock locks, the pin is refused withredirect_yarn_classic_dep_manifest_unlocked(lock the new descriptor first, for example withyarn add, then re-run). When every descriptor is already locked, the entry's dependency sub-maps are rewritten to match (redirect_yarn_classic_dep_manifest_rewritten). Vendored mode refuses the same case withvendor_dep_manifest_unlocked. If the download, the check or thepackage.jsonread fails, the patch is skipped asnpm_tarball_unavailable. A project that setsyarn-offline-mirroris refused withredirect_yarn_classic_offline_mirror. The mirror is resolved the way yarn 1 resolves it: the project's.yarnrc/.npmrc, the user's (~/.yarnrc, whichyarn config setwrites, and~/.npmrc),<prefix>/etc/yarnrc/npmrc, every ancestor directory's, and theYARN_*/npm_config_*environment variables. A file saved with a UTF-8 BOM counts too, andfalsein a higher-precedence layer turns the mirror off. Yarn looks mirror tarballs up by file name, and the hosted tarball has the same name as the upstream one already in the mirror, so installs would get the unpatched bytes and fail the integrity check. Use--mode vendoredthere; it works with a mirror. - yarn berry — the redirect pins the way yarn does for a root
resolutionsentry (cacheKey10c0/ yarn 4):package.jsonroutes the locked descriptor ("left-pad@npm:^1.3.0") to the hosted tarball and only thatyarn.lockentry is re-keyed by it. A dependency declared through a yarn catalog ("catalog:", yarn 4.10+) is matched before yarn expands the catalog, so each.yarnrc.ymlcatalog that resolves to the locked range is routed too ("left-pad@catalog:","left-pad@catalog:<name>"). Yarn then fetches it without npm registry credentials and hardened mode accepts it. The re-keyed entry is written the way yarn writes it, with its fields in yarn's order and itsbin:map taken from the served tarball's ownpackage.json. Yarn's npm resolver gives a package that runsnode-gypin a script (every package shipping abinding.gyp: nan, bufferutil, utf-8-validate, …) an implicitnode-gyp: "npm:latest"dependency, which yarn never gives a tarball entry, so the pin drops it and, when nothing else needs node-gyp, the lock entries only it reached; vendored mode does the same, andvendor --revertputs them back. Hostedrollbackputs the dependency back while the lock still resolves node-gyp; otherwise it warnsyarn_berry_node_gyp_unresolved(runyarn installonce). For an entry that has abin:map or that implicit dependency, the scan downloads the served tarball to read it. If that download fails, the patch is skipped asnpm_manifest_unavailable. A user-authoredresolutionsentry for the package is never overwritten (redirect_yarn_berry_resolutions_conflict), and.yarnrc.yml'scompressionLevelmust stay 0. The node-modules linker is e2e-covered; PnP is untested for hosted — the lock rewrite fires, but PnP's.yarn/cacheresolution isn't exercised. CRLF locks — what yarn writes on Windows, and what acore.autocrlfcheckout produces anywhere — are rewritten in their own line ending (a BOM is kept); a lock or rootpackage.jsonmixing CRLF and LF is refused (redirect_yarn_berry_mixed_line_endings, the same decision vendored mode takes) untilyarn installnormalizes it. See yarn berry compatibility. - yarn
npm:aliases (classic & berry) — a lock entry that consumes the patched package only through an alias descriptor ("safe-pad@npm:left-pad@^1.3.0") is left untouched, with aredirect_yarn_classic_alias_skipped/redirect_yarn_berry_alias_skippedwarning naming the entry — that copy keeps the unpatched artifact. The reverse shape — an alias of the patched NAME pointing at a different package ("left-pad@npm:some-fork@^1.3.0", the fork-substitution idiom) — is never rewritten: it resolves a different package. - yarn classic and yarn 2+ — installing with yarn 2+ (berry) migrates a classic (v1)
yarn.lockand re-resolves every entry from the registry, dropping hosted pins and vendored wiring alike, so the packages install unpatched. A run that leaves such a pin warns (redirect_yarn_classic_berry_migration_risk/yarn_classic_berry_migration_risk) unlesspackage.jsonpins yarn classic through"packageManager": "yarn@1…". - yarn classic git dependencies — yarn 1 fetches a git pattern (
git+https:,git+ssh:,git:,ssh:, a….giturl, or a barehttps://github.com/<owner>/<repo>) with git, using the lock entry'sresolvedas the remote, so a rewrittenresolvedbreaks every install. Hosted and vendored modes leave such an entry untouched (redirect_yarn_classic_git_skipped/vendor_yarn_classic_git_entry_skipped) and that copy stays unpatched;vexnever attests the package from that lock while the git copy is there, and rollback refuses a hosted pin an older release wrote on one. - yarn classic non-registry tarballs — a
file:tarball (left-pad@file:./x.tgz), a URL (left-pad@https://host/fork.tgz) or a hosted-git shorthand (owner/repo,github:owner/repo, which yarn locks to a GitHub codeload tarball) is the project's own artifact, not the registry package the patch is built for. Hosted and vendored modes both pin to Socket's build of the registry package, so they leave such an entry untouched (redirect_yarn_classic_non_registry_entry_skipped/vendor_yarn_classic_non_registry_entry_skipped) and that copy stays unpatched; rollback refuses a hosted pin an older release wrote on one, since its originalresolvedwas never recorded. Such a copy an older release already pinned installs Socket's build, not an unpatched one: a hosted re-run names it (redirect_yarn_classic_non_registry_legacy_pin, restoreyarn.lockfrom version control to undo it), and a vendored re-run keeps the existing wiring in sync and names it (vendor_yarn_classic_non_registry_legacy_wiring, undone byvendor --revert). - yarn classic entries with no
resolved— a registry entry with noresolvedline is a stale lock with no tarball to repoint;yarn installre-locks it from the registry. Hosted mode leaves it untouched and names it (redirect_yarn_classic_unresolved_entry_skipped); vendored mode skips it (vendor_link_entry_skipped). - yarn classic
file:directory dependencies — yarn 1 copies afile:directory (an entry with noresolvedtarball) intonode_modules, so no lock rewrite reaches that copy. Hosted and vendored modes leave the entry untouched (redirect_yarn_classic_directory_skipped/vendor_link_entry_skipped, naming it) and that copy stays unpatched;vexnever attests the package from that lock while the copy is there. This holds whatever dependency name the project gives the copy: yarn 1 locks"lp2": "file:./lpdir"aslp2@file:./lpdir, so which package it is comes from the directory'spackage.json(and, forvex, afile:tarball's manifest or a registry tarball URL's path; hosted and vendored scans name a URL copy withredirect_yarn_classic_non_registry_entry_skipped/vendor_yarn_classic_non_registry_entry_skipped). A git copy under another name records no package name in the lock and is not detected. When every entry of the package is such a copy (git,file:directory, non-registry tarball orlink:), vendoring refuses withvendor_lock_entry_not_rewritablenaming them, sinceyarn installcan't help. - bun — text
bun.locklockfileVersion 0, 1 or 2: 0 is the--save-text-lockfileopt-in lock of Bun 1.1.39–1.1.45, 1 the 1.2–1.3 default, 2 the 1.4+ default; all three emit onepackagesgrammar, so registry entries rewrite identically. Any other or missing version, or apackagessection outside bun's single-line grammar, is refusedredirect_bun_lock_unsupported(a newer version means "update socket-patch" — re-locking would reproduce it). A version-0 lock holdingworkspace:packages is refusedredirect_bun_workspace_unsupported: frozen installs of that grammar cannot keep the hosted tuple; deletebun.lockand re-lock with Bun ≥ 1.2 (which writes lockfileVersion 1, accepted) — a plain in-placebun installbumps the version only when a workspace depends on another workspace (root → member), otherwise Bun 1.2.0 keeps version 0 and 1.2.23+ fail to resolve. Version-1 and version-2 workspace locks (nested versions included) are rewritten. Binarybun.lockbfiles are read and patched natively in hosted and vendored modes, including lockfile-only discovery. The CLI updates package resolutions, integrity records and Bun's metadata hash without spawning Bun or creating a text lock. Textbun.locktakes precedence when both exist; malformed binary locks fail closed before patching. Hosted → vendored and vendored → hosted conversions both work in place (mode takeover) — on a lock the vendored backend refuses (a pre-version-2workspace:lock)vendorreports the refusal before the upstream restore and leaves the purl hosted-patched — androllback <purl>/remove <purl>restore one of several hosted bun packages to its upstream registry entry. A hostedbun.lockbentry is not rolled back (v5.0 keeps no hosted ledger, and a rebuilt binary record is not byte-exact for every lock): rollback and remove refuse it with thegit checkout -- bun.lockbremedy (thenbun install --force: a plain install keeps the patched copy), while the hosted → vendored takeover rebuilds its npm registry record natively and vendors over it. Each restore reads the package's version document from the registry Bun resolves it against (.npmrc/bunfig.tomlscope and default registries,BUN_CONFIG_REGISTRY/NPM_CONFIG_REGISTRY), read from the project's files and the user's own:$XDG_CONFIG_HOME/.npmrcor~/.npmrc, and the global$XDG_CONFIG_HOME/.bunfig.tomlor~/.bunfig.toml(#1276). Bun ≥ 1.4 takes a bunfig key over an.npmrcone and Bun ≤ 1.3 the reverse, so when they disagree on a package of a lockfileVersion-0/1bun.lockor abun.lockb(either Bun may install it) the restore refuses that pin with the checkout remedy. It sends the credentials those settings give it — a bunfigtokenorusername/password($VARexpanded only forNPM_TOKEN,NODE_AUTH_TOKENandBUN_AUTH_TOKENin the project's files, any variable in the user's own; any other variable expands to nothing, so a project's config cannot send other secrets; in a registry URL only itsuser:password@part expands, which is sent as theAuthorizationheader and never printed or written into the lock), else the.npmrc//host/path/:_authToken/_auth/username+_passwordcovering the registry URL (BUN_CONFIG_TOKENis not read); a registry that still cannot be read falls back to the default registry's document with anupstream_registry_fallbackwarning. Bun's hoisted linker keeps an installed copy whose lock entry returns to the registry record (a plainbun installreports no changes), so afterrollback,removeorvendor --revertthe patched bytes stay innode_modulesuntilbun install --force(or deletingnode_modules);redirect_bun_reinstall_required/vendor_bun_reinstall_requiredsay so whenever such a copy may be installed. Bun verifies the sha512 of URL and local-tarball tuples only from 1.3.10 (registry tuples from 1.2.0), so on 1.1.39–1.3.9 a hosted or vendored rewrite removes digest enforcement for the patched package. Every boundary here is measured against real Bun releases — see Bun compatibility. - vlt —
vlt-lock.jsondefault-registry nodes of the patchedname@versionkeep their DepID and get the patched sha512 in slot [2] and the hosted URL in slot [3]; see vlt notes.
A local scan collects the project root's node_modules and every
node_modules found in the directories below it (workspace members, nested
projects), at any depth. The walk does not descend into symlinked
directories, hidden directories (.git, .cache, ...), node_modules
itself (packages are read from it, not searched for workspaces), or
directories named dist, build, coverage, tmp, temp, __pycache__
or vendor. It also prunes every directory that carries a
Cache Directory Tagging CACHEDIR.TAG file
beginning with the standard signature — a cargo target/, and caches of
other tools that follow the convention: neither that directory's own
node_modules nor anything below it is crawled. The scan root is always
crawled, even when it is tagged itself. A CACHEDIR.TAG that lacks the
signature, is a directory or is a symlink prunes nothing.
Inside each node_modules, the package stores of isolated layouts are
walked too, since they are the only home of transitive dependencies:
pnpm's virtual store (node_modules/.pnpm, pnpm <= 3's
node_modules/.registry.*, or the directory a virtualStoreDir setting
moved it to, as recorded in node_modules/.modules.yaml), vlt's
node_modules/.vlt, Bun's isolated-linker store node_modules/.bun,
Deno's isolated nodeModulesDir store node_modules/.deno, and
node_modules/.store, written by npm's install-strategy=linked and by
Yarn 4's pnpm linker (where each entry's package/ dir is the copy). A recorded virtual store outside the project is not
listed: other projects on the machine load the same files, so patching it
in place would patch them as well. For pnpm's global virtual store
(enableGlobalVirtualStore, <store>/v<N>/links under the pnpm store
directory), only the entries this project reaches are walked: its
node_modules/<dep> links into the store, and each entry's dependency
links on to other entries. A workspace member's node_modules has no
.modules.yaml of its own (pnpm writes it only at the workspace root), so
the root's record is used, and the member's own links seed the walk. Agent-mode apply and rollback then fail on
those copies, direct and transitive alike, instead of writing through
them, and never report a transitive one as "not installed". Bun's global
store ([install] globalStore = true in bunfig.toml, or
BUN_INSTALL_GLOBAL_STORE=1, Bun 1.3.14 and later) is handled the same
way: each node_modules/.bun/<entry> is then a link into
<cache>/links/<entry>-<hash> in Bun's install cache, and those linked
entries are walked (so vex checks their bytes) but refused by agent-mode
apply and rollback. PDM's symlink install
cache gets the same treatment: with install.cache and
cache_method = symlink (PDM 2.0–2.12), site-packages/<pkg> links into
<cache>/packages/<wheel>/lib, and that package is refused too. The error
names the store and how to get a private copy (disable the global virtual
store or Bun's globalStore, or pdm config install.cache_method hardlink, then reinstall), or
use hosted or vendored mode.
Agent-mode apply and rollback also fail, dry run included, on a
node_modules/<dep> (or node_modules/@scope/<dep>) link whose real path
is outside every node_modules tree. Package managers link that way only
to first-party source: an npm, Yarn, pnpm or Bun workspace member, a
file: or link: directory dependency, or an npm link target. That
source is not an installed copy of the registry package, and no reinstall
restores it, so it is never overwritten. Patch it directly instead (vendored
mode refuses it the same way, with vendor_workspace_member; when an npm lock
also holds a registry copy of the same name@version, that copy is still
vendored and the local source is skipped with vendor_workspace_member_skipped).
Links into a store inside a node_modules tree, including a workspace member's link
into the root node_modules/.pnpm, are patched as usual. So are links into
Yarn's pnpm-linker store relocated outside node_modules, but only for an
active Yarn pnpm install (a yarn.lock, and nodeLinker: pnpm with
pnpmStoreFolder in the nearest .yarnrc.yml), only to the
<store>/<entry>/package directory of a package Yarn installs as a copy
(npm:, peer-instantiated virtual:, a file: tarball, patch:, a URL or
git; never workspace:, portal: or link:), and only when that store
does not contain the project.
The same rule covers every ecosystem, as a containment check rather than a
list of known stores: agent-mode apply and rollback refuse, dry run
included, any directory they would write into that resolves outside the
install tree it was found in. Each directory a patch writes into must
resolve inside the package directory (a flit install --symlink package
linked from site-packages into its source is refused), and a Composer
package must resolve inside its vendor dir: a Composer path repository
symlinks vendor/<ns>/<name> to your own source by default, and that
source is patched directly, or installed as a copy with the repository's
symlink option set to false. A package that a package manager links into
site-packages from its own prefix (Homebrew links formula Python packages
in from the Cellar, Nix from the store) resolves outside the install tree
too and is refused the same way; install it into a virtualenv to patch it.
rollback refuses the same directories: a file an older socket-patch
patched in place there is restored from that source's version control.
Every command that looks for installed npm copies walks these same trees, not
only scan. A package installed only under a pruned directory is therefore
"not installed" to scan --prune / --sync, which garbage-collect its
manifest entry and blobs unless a lockfile still resolves it, and apply,
rollback, remove, repair, vendor and vex do not find that copy:
remove drops the manifest entry but leaves the copy's patched files in
place.
A Rush repo has no root package.json/lockfile pair — its pnpm source-of-truth locks
live at common/config/rush/pnpm-lock.yaml (plus one per subspace under
common/config/subspaces/<name>/).
- Hosted ✅ —
scan --mode hosteddiscovers and repoints those locks in place (subspaces included). On pnpm >=11 the install needs extra Rush settings (below). - Agent ✅ — works through the generated project symlink farm.
- Vendored ❌ — refused (
vendor_rush_unsupported):rush installcopies the lock intocommon/tempand runs pnpm there, so vendor's relativefile:specs can't survive the copy — the refusal routes you to hosted mode.
Editing a Rush lock outside rush update desyncs the pnpmShrinkwrapHash in
common/config/rush/repo-state.json (with subspaces enabled, in the
common/config/subspaces/<name>/repo-state.json beside each subspace lock), so when preventManualShrinkwrapChanges is enabled
rush install fails until rush update refreshes it (a redirect_rush_repo_state_stale
warning flags this; the redirect survives the refresh — pnpm keeps locked resolutions for
unchanged specifiers).
On pnpm >=11 a repointed lock does not install under Rush's default flow: rush install
either fails (ERR_PNPM_TARBALL_URL_MISMATCH /
ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION) or, on pnpm 11, exits 0 after silently
re-resolving the patched entries to the upstream registry. Rush runs pnpm in common/temp
with a pnpm-workspace.yaml it generates, so the trustLockfile auto-config is not written
in a Rush repo; the redirect_pnpm_trust_lockfile warning gives the Rush remedy instead
(verified with Rush 5.180.0):
- pnpm 12: install with
pnpm_config_trust_lockfile=true rush install(set it in CI too). - pnpm 11: also set
"usePnpmFrozenLockfileForRushInstall": trueincommon/config/rush/experiments.json, sorush installstops passing--no-prefer-frozen-lockfile. - pnpm <=10: nothing extra.
Run rush purge before rush install so a warm store or old node_modules can't serve the
upstream files, then verify with socket-patch vex. Don't rebuild the lock
(rush update --full): that discards the hosted patches.
vlt is supported in agent and hosted mode on every release from
0.0.0-1 to 1.2.0, and in vendored mode from 0.0.0-19 (the first release whose lock has a
lockfileVersion; older locks are refused with vendor_lockfile_version_unsupported),
excluding the broken and never-published releases listed in
vlt compatibility. vlt's lock is
vlt-lock.json; its installed tree is node_modules/.vlt/<DepID>/node_modules/<name>
plus the hidden lock node_modules/.vlt-lock.json, which vlt trusts as the installed
graph. From 1.2.0 vlt also keeps a machine-wide content store (store-linker, hardlinked
on Linux by default).
Eras. The lock format and the DepID grammar changed several times, and every mode reads all of them:
| Era | Releases | lockfileVersion |
DepIDs |
|---|---|---|---|
| A0 | 0.0.0-1, 0.0.0-11 … 0.0.0-18 | absent | legacy ·/§ (··name@ver) |
| A | 0.0.0-19 … 1.0.0-rc.8 | 0 |
legacy, default registry '' |
| B | 1.0.0-rc.9 … rc.14 | 0 |
legacy, default registry npm |
| C | rc.15 … rc.32 | 1 |
tilde (~npm~name@ver), 3-tuples |
| D | rc.33 … 1.0.7 | 1 |
tilde, the registry URL in slot [3] |
| E | 1.0.8 … 1.1.1 | 1 |
tilde, hashed peer extras (~peer.<hex>) |
| F | 1.2.0 | 1 |
as E, plus the global store |
A lock socket-patch cannot read exactly as vlt does (a UTF-8 BOM, another
lockfileVersion, a nodes section outside vlt's one-node-per-line layout) is refused:
redirect_vlt_lock_unsupported (hosted) or vendor_lockfile_version_unsupported
(vendored). Agent mode never needs the lock.
Hosted: the slot rewrite. Only nodes on vlt's default registry (the '' / npm
segment, or a URL segment equal to the lock's scalar registry) are redirected, every
peer and modifier variant of the name@version included. Instances under a named alias,
a scoped registry or jsr stay untouched (redirect_vlt_custom_registry_skipped) and keep
the run's --vex from attesting the package. options is never edited: vlt fails vlt ci on any change there. Before anything is written, each artifact is fetched the way
vlt fetches it (accept-encoding: gzip;q=1.0, identity;q=0.5, raw body hashed); vlt
fails EINTEGRITY on a content-encoded response, so such a dependency is withheld
(redirect_vlt_artifact_unverifiable) instead of pinning a lock vlt ci cannot install.
Hosted: which lock confirms. vlt drives a project when its install state
(node_modules/.vlt-lock.json or node_modules/.vlt/) is present or no other npm-family
lock is. Then only vlt-lock.json can confirm a redirect, and a dependency vlt refuses is
refused for every lock. With a sibling package-lock.json (or another npm-family lock)
and no vlt install state, both are rewritten and redirect_vlt_sibling_lockfiles asks you
to delete the lock your installs do not use.
Hosted: reinstall and heal. vlt never re-extracts an installed package when only its
lock integrity or URL changes, so after a rewrite the installed tree is stale. scan and
get --mode hosted remove node_modules/.vlt-lock.json and each stale
node_modules/.vlt/<DepID> of a Socket-owned node, so the next vlt install extracts the
patched packages; rollback and remove do the same for the registry bytes. Nothing
outside the project, no link target and no copy socket-patch cannot judge is ever
removed. A stale copy of an optional dependency is kept, because vlt install does not
put back a removed optional dependency; the advisory says to run vlt ci instead, and to
upgrade to vlt 1.0.5 first when every dependency is optional (vlt 0.0.0-30 … 1.0.4 install
no optional dependency from the lock of such a project; mixed projects install it on every
release). The advisory names the kept copies by what they are: unpatched copies after
scan/get, patched copies after rollback/remove, and, after a hosted → vendored
takeover, the installed copies of the now-vendored optional dependencies (whatever bytes
the hosted pin left installed). --no-vlt-install-cleanup (or
SOCKET_NO_VLT_INSTALL_CLEANUP) keeps the tree. redirect_vlt_reinstall_required says
what happened and what to run; a stale or unchecked copy is never attested by the run's
--vex.
Hosted: vlt behaviors to know.
- The pin survives
vlt ci, frozen installs andvlt install <new>, except that 1.0.0-rc.6 … rc.17 drop slot [3] of every default-registry node on a re-save and keep the patched integrity: the nextvlt cithen failsEINTEGRITY(loudly, never silently unpatched) untilscan --mode hostedre-pins, androllbackreports drift with that remedy. vlt updatere-resolves an unchanged exact spec from 1.0.8 on, dropping the redirect (and the run's VEX then no longer attests it); 0.0.0-20 … 1.0.7 keep the locked node.- vlt enforces tarball integrity only on a cold fetch (every release except 0.0.0-1): a warm cache keyed by URL serves stale bytes even when the integrity differs. Hosted artifact URLs are immutable per artifact for that reason.
- 0.0.0-16 … 0.0.0-24 ignore
vlt-lock.jsonunlessvlt.jsondeclares"modifiers": {}(redirect_vlt_old_lockfile_ignored), rc.7 … rc.29 re-resolve from public npm when a scalarregistryis configured (redirect_vlt_scalar_registry_ignored), and a lock with nolockfileVersionis silently re-resolved by vlt ≥ rc.15 (redirect_vlt_lockfile_version_missing). Each keeps the run's VEX from attesting. - Documented gap: a
lockfileVersion1 lock that sets bothregistryandregistries.npmmay come from rc.15 … rc.29 (which ignore the lock) or from ≥ rc.30 (which do not); the lock cannot tell them apart, and 1.x users commonly set both, so no warning fires for it. - From rc.33 every install,
vlt ciincluded, needs registry config (vlt.jsonconfig.registries.npm/config.registry,VLT_REGISTRY, orvlt setup), and from 1.0.5registries.npmspecifically. socket-patch never writesvlt.json.
Vendored: directory artifacts (the D19 layout). A direct dependency of the root or of
a workspace member is vendored as a patched package directory,
.socket/vendor/npm/<uuid>/<name>-<version>/node_modules/<name>/, never a tarball: vlt
links a file: directory in place, and the extra node_modules/<name> level lets a
package that require()s its own name resolve itself. The payload's devDependencies
are dropped from its package.json (vlt would otherwise try to install them). The uuid
directory carries a .gitignore that re-includes the payload (!*) against the project's
own ignores (node_modules, dist/, *.map, …) while ignoring the links vlt creates
inside it, and a .gitattributes that turns EOL conversion off so an autocrlf checkout
stays byte-exact. The lock's node becomes a file node, the importer edges and the
importers' package.json specs move to the file: path, and every moved entry is placed
where vlt's own serializer puts it, so vlt ci keeps the lock byte-identical. Transitive
targets are refused (vendor_vlt_transitive_unsupported: vlt silently reverts
transitive lock surgery), as are several instances of one name@version, modifier
variants, importer peer edges, foreign registries, a git, remote-tarball or local-directory
node of the same package and a dependency declared in several fields
(vendor_lock_entry_unsupported), and a package with a preinstall, install,
postinstall or prepare script or a binding.gyp, which vlt build would build inside
the committed directory (vendor_vlt_build_scripts_unsupported); use hosted mode for those. From vlt 1.0.8 a root
dependency with resolved peers carries a ~peer.<hex> extra even with one peer context (a
workspace member's a ~peer.N one from rc.15): vendored mode writes its file node
without the extra, exactly as vlt writes a file: dependency, keeps its peer edges, and
vendor --revert restores the extra. vlt 0.0.0-31 … rc.5 cannot reinstall a vendored
file: dependency without the lock (vendor_vlt_legacy_lockfile on era-A locks: ·· ids,
or URL-segment ids equal to a scalar registry), and A0 locks are refused. From 0.0.0-30 a
plain vlt install keeps an optional dependency's installed upstream copy after
vendoring; vendor_vlt_reinstall_required says to run vlt ci (or delete node_modules
and run vlt install) to link the vendored directory, and to upgrade to vlt 1.0.5 first
when every dependency is optional (0.0.0-30 … 1.0.4 install none from the lock). The same advisory names any dependency whose
node_modules link still resolves to vlt's store, and vendor --revert repeats it for an
optional dependency (a plain vlt install keeps the link to the removed vendored
directory) or a link still into the vendored directory.
Agent mode. apply and rollback patch every store copy of a name@version
(legacy and tilde DepIDs, peer and modifier variants, transitive-only packages) and
replace each file instead of writing through it, so vlt 1.2's shared store (hardlinked on
Linux) and every other project linked to it keep their bytes. A patch survives vlt install, install <new>, uninstall and frozen installs; vlt ci and deleting
node_modules restore the upstream bytes; run socket-patch apply after them.
Locale. vlt sorts its lock with the process locale for one key, so byte-stability is
claimed only for en-equivalent locales (LANG unset, C, POSIX or en_US); every vlt
test job sets LANG=C and LC_ALL=C. Vendored ownership and revert match by entry text,
so they do not depend on the order.
Compatibility of ledgers. vlt vendor ledgers (flavor: "vlt" entries) require the
socket-patch release that adds vlt support: an older release does not understand them.
Hosted vlt pins need no ledger (v5.0): rollback restores them from the npm registry.
Honest limits of the Maven and NuGet flows — documented behavior, not bugs:
-
Fail-closed by version suffixing (hosted Maven). Maven has no lockfile, so hosted mode pins the patch a different way: the Socket patch server (
patch.socket.dev) exposes the patched jar under a globally-unique<version>-socket.<hex8>suffix that exists only on the injectedsocket-patch-<uuid>repository. The rewriter pins that suffixed version explicitly — it rewrites the literal<version>, or (for a transitive / managed dependency with no literal version in your pom) adds a<dependencyManagement>entry — so a resolver that can't reach the Socket repo, or is handed different bytes, has nowhere to fall through to: the build hard-fails instead of silently resolving the unpatched upstream artifact. The<repository>'schecksumPolicy=failstill verifies the transport-level.jar.sha1sidecar on top. A${property}version is refused (redirect_maven_dep_unpinned) — a literal edit would break the property reference and a depMgmt pin could strand sibling artifacts sharing the property. A literal version that matches neither the base nor the suffixed value is skipped (redirect_maven_dep_version_mismatch). A<base>-socket.<hex8>literal left by an earlier hosted patch for the same release (the pom declares that patch'ssocket-patch-<uuid>repository) is re-pinned to the new suffix: the supersededsocket-patch-<uuid>repository and its Trusted Checksums entries are removed, and a repository whose grant URL changed (a rotated token) is refreshed in place. A suffixed literal no hosted repository in the pom minted (a vendoredsocket-patch-vendor-<uuid>pin, say) is still a mismatch and is skipped. -
Multi-module reactors are vendored-only (hosted Maven). Hosted mode reads only the root
pom.xml, and a module's own literal<version>always beats a root<dependencyManagement>pin, so a root pin would leave that module on the unpatched upstream jar. A root that declares<modules>or<subprojects>(directly or in a profile) is therefore refused withredirect_maven_multimodule_unsupported(pom.xmland.mvn/are left untouched and the dep is not counted as redirected); usescan --mode vendored, whose reactor planner rewrites each module's declaration.vexlikewise does not attest a hosted pin found in a reactor root. -
Trusted Checksums reinforcement (hosted Maven, 3.9.4+). When the patch server supplies both the jar and pom sha256, the rewriter also emits Maven Trusted Checksums files —
.mvn/maven.configresolver args plus.mvn/checksums/checksums.sha256entries pinning both artifacts under the suffixed version's local-repo path (merging into any pre-existing user config / checksum set; a conflicting value is never overridden and surfacesredirect_maven_trusted_checksums_conflict). This is an independent client-side content pin on top of the transport check. It requires Maven 3.9.4+. Older releases leave the.mvn/*files inert, and only the transport.sha1check guards the Socket-served bytes. The version suffixing above still fails closed on its own.- 3.9.0 / 3.9.1 do not interpolate the
${session.rootDirectory}basedir the config uses, so the summary file is never found. - 3.9.2 / 3.9.3 ignore
checksumAlgorithms=SHA-256and check SHA-1 only. - Below 3.9 there is no Trusted Checksums post-processor.
When
.mvn/wrapper/maven-wrapper.propertiespins a Maven older than 3.9.4, the rewriter still writes the files and warnsredirect_maven_trusted_checksums_unenforced. Without a Maven Wrapper, the CLI can't tell which Maven builds the project, so it doesn't warn. On Maven 3.9.4–3.9.8 a mismatch is enforced but reported unclearly; the readability fix landed in 3.9.9 (MNG-8182). The args areoriginAware=falseandfailIfMissing=false, so one checksum matches the artifact from any repository and a dependency with no committed checksum still resolves — only a mismatch fails. - 3.9.0 / 3.9.1 do not interpolate the
-
Local-repository discovery reads coordinates from the path.
scan(and every other crawl of~/.m2/repository) takes a POM's groupId / artifactId / version from its directory when the file sits at the canonical<group path>/<artifactId>/<version>/<artifactId>-<version>.pom— the only place Maven writes one — without opening it. The path spells the group relative to the scan root, so each top-level group directory (org/,com/, ...) is confirmed first: the first canonical POM under it whose contents parse must name the coordinates its path does. A directory that fails that check — all of them when--global-prefix/SOCKET_GLOBAL_PREFIX/MAVEN_REPO_LOCALnames a directory above or inside the repository rather than the repository itself — is read content-first, as before. Any other.pom(a SNAPSHOT dir's timestamped POM, a hand-placedextra.pom, a group directory whose name holds a.) is parsed as before, falling back to the directory path when the POM names no usable coordinates. The one visible consequence: under a confirmed directory, a POM at a canonical path whose contents disagree with its directory (hand-placed, or a legacy upstream POM with mismatched coordinates) reports the directory's coordinates. -
Warm
~/.m2copies. Vendored and hosted Maven both pin a suffixed<version>-socket.<hex8>, so a cached copy of the original version in~/.m2cannot shadow the patch. Audit conflicting copies of that suffix withvendor --check --local-repo <path>. A project vendored before v5 (the same-GAV<repository>wiring, which a warm~/.m2could shadow) is refused withlegacy_maven_rootuntil you runsocket-patch vendor --revertand vendor again. -
No Maven Wrapper (vendored). Without
.mvn/wrapper/maven-wrapper.propertiesthe CLI can't tell which Maven builds the project, sovendorwarnsvendor_jvm_degradedtwice:maven_f_outside_root(Maven 3.9.2–3.9.8 can't read the vendored tree with-ffrom outside the project) andmaven_mirror_of_all(Maven before 3.9.2 reads the fallback file repository, which amirrorOf *mirror captures). The patch is still applied. To clear them, add a Maven Wrapper pinned to Maven 3.9.9 or later, or make sure yoursettings.xmlmirrors excludesocket-patch-vendor(for example<mirrorOf>external:*</mirrorOf>or<mirrorOf>*,!socket-patch-vendor</mirrorOf>). -
mirrorOfmirrors (hosted Maven). Asettings.xml<mirror>with<mirrorOf>*</mirrorOf>(common in corporate environments) reroutes all repositories — including the injectedsocket-patch-<uuid>repository — through the mirror. Because the patch resolves only at the suffixed version, the mirror (which does not carry it) can't serve it and the build fails loudly rather than silently going unpatched. Scope the mirror to exclude the Socket repos (e.g.<mirrorOf>*,!socket-patch-*</mirrorOf>) so the redirect resolves; theoriginAware=falseTrusted Checksums act as a backstop when present. -
Gradle (hosted Maven). Gradle builds get automated wiring, not a pasted snippet: an owned settings script pins the suffixed version, every lock file is rewritten, and a tripwire fails the build if the base version still resolves. See Gradle for the rules and refusals;
redirect_gradle_manual_snippetnow appears only beside a refusal. -
NuGet locked mode (hosted + vendored). With a
packages.lock.jsonanddotnet restore --locked-mode, the rewrittencontentHashpins the patched.nupkg— a tampered or wrong package fails restore withNU1403. Without a lockfile there is no client-side content pin (vendored surfaces this as avendor_nuget_no_lockfilewarning; the feed + source mapping still force the patched copy). -
NuGet package signatures. The server constructs the patched
.nupkgand removes invalidated upstream signatures. The CLI verifies and stores the served archive without repacking it. Vendoring uses a folder feed with source mapping; installations requiring signed packages need an appropriate server artifact and signing policy.
Gradle builds are part of the maven ecosystem: patches are Maven PURLs and every
mode works on Gradle 6.8 or newer with Groovy or Kotlin DSL. The test grid covers
Gradle 6.9.4, 7.6.6, 8.14.3 and 9.8.0 on Linux, macOS and Windows (see
testing). The machine-readable contract, with every code,
is the "Gradle builds" section of
CLI_CONTRACT.md.
Which mode to pick. Hosted and vendored mode change files you commit, so every
checkout and CI runner builds the patched jar. Agent mode rewrites the Gradle cache
of one machine: it is undone by --refresh-dependencies, shared by every build that
uses the same Gradle user home, and refused when the build verifies its
dependencies. Prefer hosted or vendored for Gradle.
scan reads Gradle's cache (<Gradle user home>/caches/modules-2/files-2.1, plus the
read-only cache in $GRADLE_RO_DEP_CACHE) before ~/.m2. The user home is found the
way Gradle finds it: -Dgradle.user.home in GRADLE_OPTS, then GRADLE_USER_HOME,
then .gradle in the account's home directory. On Unix that is the passwd entry, not
$HOME; when they differ, scan notes gradle_user_home_differs.
A Gradle-only build reads ~/.m2 only through mavenLocal(). Scan looks for it in
every settings, build, buildSrc, included-build, applied and init script, including
the wrapper distribution's init.d. Without it, ~/.m2 is not scanned, and modules
found only there are listed in a gradle_build_ignores_m2 warning. When a script
cannot be read literally, ~/.m2 stays in the scan and scan notes
gradle_maven_local_undetermined. In JSON, packages from the Gradle cache carry
inLock when the build's lock files were read.
apply patches every copy the build consumes. Gradle keeps one hash directory per
download of a version, and each one holding the patched files is patched. Records
keyed by members inside a jar swap in the patch service's build of the whole jar, so
they need network access (jvm_agent_service_required offline). The original is
backed up under .socket/jvm-originals/ and rollback restores from it. rollback
and remove restore every patched writable copy, including a ~/.m2 copy the build
no longer reads (an earlier apply patched it while mavenLocal() was declared).
Such a copy never fails the run: one another build re-patched or rebuilt, or whose
original this project never backed up, is left as it is with
gradle_m2_copy_not_restored.
Guards:
gradle/verification-metadata.xmlpresent: refused (gradle_verification_metadata_present). Gradle would reject the rewritten bytes, so use hosted or vendored mode.- The only copy is in
~/.m2, which the build never reads: refused (gradle_build_ignores_m2). Build once so Gradle caches the jar, then apply. - The only copy is in
~/.m2and the build declaresmavenLocal(): patched, with the warninggradle_m2_may_be_unconsumed. When another repository comes beforemavenLocal(), Gradle downloads the module from it instead; build once and apply again. - Read-only cache shadowing. A copy in
$GRADLE_RO_DEP_CACHEis never written, and Gradle may read it first. The writable copies are patched but the run fails (gradle_ro_cache_shadows). Rebuild the read-only cache from a patched user home. - A cache copy whose bytes are not the ones the patch was made for is left alone and
fails the run (
gradle_copy_unexpected_bytes). - Transform-copy staleness. Gradle keeps derived copies of jars
(
caches/transforms-*,caches/jars-*, instrumented build-logic jars). One proven to come from the unpatched jar fails that copy (gradle_transform_copy_stale); one that cannot be matched is reported (gradle_transform_copy_unverified). Rungradle --stop, delete the named directories, and apply again. - On Windows a jar held open by a daemon is reported as
gradle_jar_locked_by_daemon. Rungradle --stopand retry.
Each patched Gradle copy also gets informational advisories:
gradle_refresh_reverts (--refresh-dependencies downloads a fresh, unpatched copy;
apply again), gradle_daemon_stale (a running daemon may still hold the old classes)
and gradle_global_cache_shared (every build of that user home sees the patch).
scan --mode hosted pins the patched jar at its Socket-only
<version>-socket.<hex8> version and writes, for you to commit:
.socket/gradle/socket-patch.hosted.settings.gradle, an owned script whose bytes change only with a CLI release, and its data,.socket/gradle/hosted-index.tsv;- one
apply fromline in every settings file of the checkout's builds: the root,buildSrcand each literalincludeBuild. A missing settings file is created and marked so thatrollbackdeletes only files it created; - every
gradle.lockfile,buildscript-gradle.lockfileand legacygradle/dependency-locks/*.lockfileentry of the patched GA, moved to the suffixed version; - the suffixed component in an existing
gradle/verification-metadata.xml.
The script routes the suffixed version to the Socket repository only
(exclusiveContent). It substitutes every request that would select the patched base
version, including transitive requests, ranges, dynamic (1.+) and rich versions. It
rejects other versions at or below the base and fails the build if one still
resolves. It also checks the jar's sha256. A request whose selector does not admit the
base (an explicit newer version, a lock or strictly above the base, a transitive
bump) is left alone, so a version above the base still resolves and an upstream fix
is never downgraded. vex withholds the statement when a lock file records such a
version (vex_gradle_lock_above_base), while list, rollback and remove still
find the pin; without dependency locking it judges the installed suffixed copies. A
dynamic or range selector that admits the base is pinned like a lock: it keeps
resolving the patched version after a newer upstream release appears
(redirect_gradle_dynamic_selector_pinned); declare the newer version to move past
the patch. A latest.release / latest.integration request is refused
(redirect_gradle_latest_selector): the script cannot rewrite it, and with every
upstream version at or below the base rejected it would not resolve until upstream
ships a newer release. The apply line carries a digest of the index, so a changed pin set
invalidates the configuration cache.
Detached configurations. Every project and buildscript configuration is pinned.
Configurations a plugin creates with configurations.detachedConfiguration are not
reached and may resolve the upstream version. Every hosted run says so
(redirect_gradle_detached_configs_unguarded).
Gradle module metadata. When the Socket repository serves a suffixed .module,
the wiring uses it silently. A deployment that does not serve one warns
redirect_gradle_module_metadata_unavailable; Gradle then resolves the suffixed pom,
so variants and capabilities that only the upstream .module declares are not
applied.
A dep is refused, with nothing written for it, when the build is outside what the
script can pin: a wrapper below 6.8, Android or Kotlin Multiplatform plugins, an
includeBuild the CLI cannot follow, a custom lockFile, the GA on a
settings-script classpath (declared, or in a settings-gradle.lockfile), a
classifier declaration, a latest.* request, a strictly constraint admitting only
versions below the base, a user exclusiveContent rule claiming the group, a lock entry below the base
(re-lock first), a GA that is vendored, or a grant that serves the original
coordinates. Each refusal has its own redirect_gradle_* code and is followed by
redirect_gradle_manual_snippet, a paste-able snippet in the build's DSL that applies
the owned script's rules (it adds no dependency, and versions above the base still
resolve).
rollback and remove restore without the network, using only the index. list,
vex and the other readers re-run the planner's build-level checks, so a build changed
after the scan in a way the pin cannot reach (a settings-classpath declaration, an
includeBuild the CLI cannot follow, …) stops attesting. A vendored Gradle package is
taken over only when the hosted planner would accept it. Otherwise it keeps its
vendored patch.
See JVM vendoring. In short: the original GAV is
committed under .socket/vendor/gradle/, a settings script serves it with
exclusiveContent and verifies its sha256, and lock files and build scripts stay
unchanged. Run it from the build root: a subproject directory is refused
(not_build_root). A root with both pom.xml and a Gradle build wires both.
Classifier jars a build declares are vendored too, and a derived maven-metadata.xml
keeps ranges on the vendored version. Existing pgp-only verification entries, and the
classifier jars the tree serves, get a checksum. Refusals use vendor_jvm_shape_unsupported /
vendor_jvm_upstream_unavailable, and partial wiring uses vendor_jvm_degraded
(VEX withheld). Each detail starts with reason: <reason>:.
vex re-hashes every copy a build may load: the ~/.m2 copies it reads, every
Gradle hash directory, and the suffixed copies of a hosted pin. A derived copy proven
to come from the unpatched jar, or an older one that does not match the patched jar,
withholds the statement (vex_gradle_unpatched_copy). A derived-cache walk cut short
on a very large cache warns vex_gradle_derived_cache_unchecked and does not
withhold.
sbt, Mill and scala-cli resolve pkg:maven artifacts, so they are build shapes
inside the maven ecosystem, not an ecosystem of their own: --ecosystems maven
covers them, and their PURLs, rollout budgets and ledgers are Maven's. A root
holding build.sbt, build.mill, build.mill.yaml, build.sc or
project.scala counts as a Maven project for discovery. Design, probes and the
exact generated bytes: sbt, Mill and scala-cli support;
the tested versions: sbt compatibility.
| Tool | Agent | Hosted | Vendored |
|---|---|---|---|
| sbt 0.13.18 – 1.2 (Ivy) | Ivy cache | socket-patch.sbt |
socket-patch-vendor.sbt + suffixed tree |
| sbt 1.3 – 1.13, 2.0 (Coursier) | Coursier cache | socket-patch.sbt |
socket-patch-vendor.sbt + suffixed tree |
| sbt older than 0.13.18 | Ivy cache (untested) | refused (redirect_sbt_unsupported_version) |
refused (vendor_sbt_unsupported_version) |
| Mill 0.11 – 1.x | Coursier cache | snippet | not wired |
| scala-cli, directory build | Coursier cache | snippet | owned files + same-GAV tree |
| scala-cli, single file | Coursier cache | snippet | pass the tree with -r (below) |
Tested: sbt 0.13.18, 1.2.8, 1.3.13, 1.9.9, 1.13.0 and 2.0.9 in every sbt mode
(JDK 8 up to 1.3.x, 17 after), Mill 1.1.10 and scala-cli 1.17.1. sbt 2.1 and
later is wired with a *_version_untested warning.
Agent mode patches the shared caches the tools resolve into, next to ~/.m2
(only for an sbt, Mill or scala-cli project: a Maven or Gradle project keeps
crawling ~/.m2 alone; --global crawls every cache):
- Coursier (sbt 1.3+, sbt 2, Mill, scala-cli):
$COURSIER_CACHE, a-Dcoursier.cache=inJAVA_OPTS/SBT_OPTS/.jvmopts/.sbtopts,-Dsbt.coursier.home, then the OS default (~/.cache/coursier/v1,~/Library/Caches/Coursier/v1,%LOCALAPPDATA%\Coursier\cache\v1). Coursier keeps hidden checksum files beside each cached file and silently re-downloads a file that disagrees with them, soapplyandrollbackrewrite them to the new bytes (the envelope'ssidecars[]lists them). - Ivy (sbt 0.13 – 1.2, or
useCoursier := false):-Dsbt.ivy.home/-Divy.home, then~/.ivy2/cache.
The crawl is not scoped to the build: as with ~/.m2, every cached GAV is
queried (a Coursier directory holding only the .pom of a version it
considered but did not pick is no copy), and a GAV cached in several roots is
patched (and restored) in every one, since the build loads whichever copy its
resolver picks: the same every-copy fan-out as Gradle, and vex
re-hashes each copy. An Ivy module's sources jar under srcs/ is found for a
?classifier=sources patch. Every project on
the machine sees the patch. A running sbt server, Bloop or Metals holds the old
classpath: restart it after apply. Agent patches are matched as whole jar
files.
scan (hosted is the default) and get --mode hosted wire an sbt build root
(project/build.properties naming sbt.version 0.13.18 or later) through one
generated root file, socket-patch.sbt. No user file is edited. For each
patched GA it:
- forces the Socket-only
<base>-socket.<hex8>version build-wide withdependencyOverrides; - adds a
file:resolver over the gitignored.socket/sbt-hosted/maven2/, moved ahead of the default repositories on sbt 0.13 / 1.x, and downloads the sha256-pinned jar and pom there on the first sbt load; - installs a load-time verifier that fails
sbt updatewhen a project resolves another version or a pinned artifact whose bytes are not pinned, or declares the GA at a version newer than the patched base (the override would otherwise force it back down).
Commit socket-patch.sbt. The first load after a fresh checkout needs to reach
the patch server; later loads work offline from the downloaded copies.
A new pin is gated on what sbt itself resolved, read from target/ (never by
running sbt): run sbt update (for the whole build) before
socket-patch scan. With no evidence, or any project's evidence older than
the build files, nothing is written and the run warns once
(redirect_sbt_no_resolution_evidence, redirect_sbt_resolution_stale) and
exits 0. A patch whose GA some project
resolves at another version, the Scala runtime, or a build that reassigns
dependencyOverrides / resolvers (:=, ~=) is refused. Re-runs re-check
existing pins against fresh evidence: after a dependency edit, sbt update
then a re-run re-verifies the pin and records the build's new dependency
digest; a build that now declares the GA newer than the patched base is
refused (redirect_sbt_pin_declared_newer: roll the patch back, or declare
the base again). rollback / remove restore the file
offline. The full code list is in
CLI_CONTRACT.md (Hosted
sbt).
vendor / scan --mode vendored write the patched jar and its pom into the
committed tree .socket/vendor/maven2/<g>/<a>/<base>-socket.<hex8>/ and one
generated root file, socket-patch-vendor.sbt, which pins every tree file's
sha256, resolves from the tree (ahead of the default repositories on 0.13 /
1.x, so the build resolves the patch with the network down) and forces the
suffixed version. Its load-time check fails the sbt build closed on a tampered
tree or a replaced pin. Commit both (socket-patch-vendor.sbt and
.socket/vendor/: without the root file the build resolves the unpatched
upstream), then run sbt update; the tree carries its own .gitignore
(!*, so a *.jar rule cannot drop the jar) and .gitattributes. The same
sbt update evidence gate as hosted applies (vendor_sbt_* codes; a skip is
reported skipped, exit 0). Vendoring over a hosted pin runs the gate before
the hosted pin is restored, so a pin the gate would stop stays hosted. The hosted
and vendored files never pin the same GA. Revert, rollback, vendor --check
and repair cover the generated file and tree.
A root holding project.scala and no pom.xml, Gradle or Mill build is a
scala-cli directory build. vendor writes only files socket-patch owns: a root
socket-patch.scala that includes a guard inside the committed same-GAV tree
.socket/vendor/coursier/, plus .socket/vendor/coursier-index.tsv. Deleting
the tree fails the build (File not found) instead of resolving upstream. The
gate reads scala-cli's Bloop project files under .scala-build/.bloop/: run
scala-cli compile --test . (with the default Bloop server) first. A build
that declares its own repository (//> using repository, -r) is refused,
because scala-cli would consult it before the tree; move it to
COURSIER_REPOSITORIES, which is consulted after. Windows and project paths
holding % or a non-ASCII character are refused.
Single-file runs (scala-cli run main.scala) ignore the directory wiring.
After vendoring the directory, pass the tree explicitly:
scala-cli run main.scala -r file://$PWD/.socket/vendor/coursier.
Mill builds get agent mode and, in hosted mode, a paste-able snippet per patch
(redirect_mill_manual_snippet): a repository plus the forced suffixed version
(Mill 1.x: repositories + depManagement, each appended to the module's own
inside Task { … }; 0.11 / 0.12: repositoriesTask + .forceVersion()). Nothing is written. vendor does not wire a Mill build: the
only committable mechanism found edits the user's .mill-jvm-opts and replaces
the repository list, so it is left as a
documented manual recipe.
- Pins need fresh
sbt updateevidence for every declared project; a build whose project definitions cannot be read statically is skipped (*_resolution_incomplete). -Dsbt.override.build.repos=truedrops the build's resolvers, so the generated file fails the load (warned at wiring time).- A
build.sbt.lock(sbt-dependency-lock) is refused; runsbt dependencyLockWriteafter wiring by hand. - Classified artifacts other than
sources/javadoc, and the Scala runtime (org.scala-lang), are refused. - The in-memory hosted engine (GitHub App) has no
target/evidence, so it never wires sbt. - Vendored sbt VEX attests through the vendor ledger; the generated file alone is not a VEX reference.
Agent mode patches the crate in place wherever the crawler finds it. For a non-vendored
crate that means the shared $CARGO_HOME/registry cache: the patch affects every
project on the machine, and is silently reset by cargo clean or a cache prune. Use
--mode vendored for a project-local, committable patch.
Run cargo fetch before apply on a fresh checkout or CI runner. Cargo unpacks a
locked crate into registry/src only when it fetches or builds, so on a cold or pruned
cache there is nothing to patch, and the next cargo build would compile the pristine
crate. apply therefore treats a crate that Cargo.lock resolves but that is not
unpacked as not installed, not as a calm lockfile-only skip: a run where no targeted
patch matched exits 1 and names cargo fetch as the remedy.
Vendored mode (v5+) wires a patched crate with a [patch.crates-io] path
entry in the workspace-root Cargo.toml (the manifest beside the
Cargo.lock it detaches) plus the lock surgery that drops the crate's
source/checksum and records the copy's tagged version:
[patch.crates-io]
cfg-if-socket-9f6b2c4e = { package = "cfg-if", path = ".socket/vendor/cargo/<uuid>/cfg-if-1.0.4" }
# a second vendored version of the same crate (needs cargo 1.45+):
cfg-if-socket-0a1b2c3d = { package = "cfg-if", path = ".socket/vendor/cargo/<uuid2>/cfg-if-0.1.10" }# Cargo.lock
[[package]]
name = "cfg-if"
version = "1.0.4+socket.<uuid>"- Tagged versions. The vendored copy's own
Cargo.tomlversion is rewritten to<version>+socket.<uuid>(a version with build metadata keeps it:2.0.1+zstd.1.5.2→2.0.1+zstd.1.5.2.socket.<uuid>), and the detached lock entry carries the same tagged version — the lock cargo itself writes for the tagged copy. Cargo ignores build metadata when matching requirements, so1.0.4,=1.0.4,1,^1in any dependent still select the copy. The lock alone therefore names the patch uuid of the copy cargo builds (a[patch]override elsewhere changes the locked version), and stripping the tag gives the purl version. Every lock reference that spells the version ("cfg-if 1.0.4", v1's full ids) is rewritten with it, in lock formats v1–v4; a lock that cannot be kept consistent refuses withcargo_lock_untaggablebefore any write. The patched crate sees the tag inCARGO_PKG_VERSION— e.g. a vendored binary crate's--versionoutput shows it; requirement matching (semver::VersionReq) is unaffected, but string comparisons and equality / ordering on a parsedsemver::Version(which compares build metadata) see it. Revert restores the original lock byte for byte — including when you later lock your own same-version path crate beside the copy: that entry is left alone and the registry entry comes back under its full id, as cargo writes it. Copies and locks vendored before tagged versions are tagged by the next re-run orrepair(cargo_version_tagged; a dry run says "would tag"). For VEX, an untagged detached lock entry counts only beside an untagged (pre-tag) copy, and a copy whoseCargo.tomlis tagged for another uuid than its path is dead wiring. - Why the manifest. Socket's scanners already ingest
Cargo.toml, so the patch uuid in the path is recoverable for SBOM annotation without uploading.cargo/config*(which can hold registry tokens), and manifest[patch]builds on cargo older than 1.56, the floor of config-file[patch](proven by the old-toolchain e2e tests, which build and run the patched copy with no network on the cargo 1.41 and 1.56 docker images — the CIcargo-old-toolchainsleg; without the images a local run falls back to type-checking on rustup toolchains). Two vendored versions of ONE crate need cargo 1.45 or newer. Cargo before 1.45 resolves every source-lessCargo.lockentry for a crate through ONE[patch.crates-io]path — the entry whose KEY sorts last — so with two vendored versions one of the two lock entries is pinned to the other version's copy andcargo build --lockedfails closed withpatch for `<crate>` … did not resolve to any crates. A populated crates.io index in$CARGO_HOMEdoes not help; whether a given pair of patch uuids happens to build there is an accident of how their keys sort. The floor was measured on one two-version fixture in both key orders (cargo check --locked --offline, empty$CARGO_HOME): 1.41.1, 1.42, 1.43 and 1.44 refuse the adversarial order; 1.45, 1.49, 1.53, 1.56 and current stable resolve either order, each lock entry to its own copy. Vendoring a second version of a crate therefore warns (cargo_multi_version_old_cargo) unless the project'srust-versionorrust-toolchain[.toml]promises cargo 1.45+ — socket-patch never runscargo, so those files are the only signal it has. On an old cargo,cargo build --offlinefrom an empty$CARGO_HOMEis enough; without--offlineit loads the crates.io index first and fails when that is unreachable. Current stable needs neither. A SINGLE vendored version still builds on 1.41. Each clause is asserted by the old-toolchain e2e test, in both directions, with the adversarial key order. - Keys. Always the Socket-owned
<name>-socket-<first 8 hex of the uuid>withpackage = "<name>"(the full uuid hex if that key is taken), never the bare crate name: cargo lets a config-file[patch]item — the project's, an ancestor directory's, or$CARGO_HOME's — replace the manifest item with the same key whatever its version, so a crate-named key could be silently shadowed by your own config. Keys any of those config files already use are avoided. Every lookup (re-run, revert, VEX discovery) is key-agnostic: an entry belongs toname@versionwhen its crate (package, else the key) isnameand its path is.socket/vendor/cargo/<uuid>/<name>-<version>. The key is a function of the patch uuid, so the order two versions' keys sort in is arbitrary — which is why cargo below 1.45 cannot be relied on for a multi-version project (above). - Your entries. User-authored
[patch.crates-io]entries are never modified. One — inCargo.tomlor any cargo config file cargo merges (project, ancestors,$CARGO_HOME) — that patches the same crate and is not provably another version (a git/registry patch, or a path whoseCargo.tomlversion is unreadable or equal) refuses the vendor withuser_authored_patch_entry. - Refusals.
cargo_manifest_not_workspace_root: run from a workspace member (cargo ignores[patch]outside the workspace-root manifest) — run from the root.cargo_manifest_patch_source_alias: the manifest also has a[patch."https://github.com/rust-lang/crates.io-index"]table, which cargo lets replace[patch.crates-io]wholesale — move its entries under[patch.crates-io]. Alsocargo_manifest_unreadable,cargo_manifest_unparseable, andredirect_symlinked_file_unsupportedfor a symlinkedCargo.toml(the one symlink code every mode uses). - Formatting. Comments, ordering, CRLF / mixed line endings, a UTF-8
BOM and the trailing-newline state are preserved; a revert with nothing
else changed restores
Cargo.tomlbyte for byte, and keeps your own[patch]/[patch.crates-io]headers (an explicit[patch], or a[patch.crates-io]that another table follows or that carries a comment). - Migration. Projects vendored by an older release carry the entry in
.cargo/config.toml(or.cargo/config). Re-runningvendor,scan/get --mode vendored, orrepairmoves it intoCargo.toml(cargo_wiring_migrated), updates the vendor ledger, and deletes a config file (and.cargo/) the move emptied; a legacy entry that cannot be removed fails the run with nothing changed (cargo_legacy_wiring_kept). A project hit by the old multi-version overwrite (a second vendored version repointed the crate-named config key, leaving the first version's lock entry detached and unwired) is healed the same way (cargo_wiring_restored). The same re-run orrepairalso tags an untagged copy and lock entry (cargo_version_tagged). Every revert removes both spellings.
Both Go modes work through a go.mod replace directive pointing at a committed
directory — .socket/go-patches/<module>@<version>/ in agent mode,
.socket/vendor/golang/<uuid>/<module>@<version>/ in vendored mode — because the module
cache is go.sum-verified, so patching it in place can't build. Go never verifies a
directory replace target against go.sum — that is by design (it's how local module
development works), and it means the committed patched tree itself is the protection:
commit it, and review it like any other vendored code. The wiring survives
go mod tidy, and apply --check gives CI a read-only audit that the committed
redirects still match the manifest.
Hosted mode uses a fork-style module replacement and two committed checksums:
# go.mod
replace example.com/module v1.4.2 => patch.socket.dev/gopatch/<uuid> v1.4.2-socketpatch.1
# go.sum
patch.socket.dev/gopatch/<uuid> v1.4.2-socketpatch.1 h1:<module-zip-hash>
patch.socket.dev/gopatch/<uuid> v1.4.2-socketpatch.1/go.mod h1:<module-file-hash>
The service supplies the replacement path, version, and both hashes; the example
version is illustrative. Both hashes must be present before the CLI writes the
replacement. Go uses committed go.sum entries without consulting the checksum
database for those entries, so a fresh checkout needs no per-machine checksum
exemption. A mismatched checksum still fails the build. The rewriter removes the
replaced version's original sum lines to keep the result stable under go mod tidy.
A re-pin to a newer patch (a superseding patch uuid, or the same patch republished
at a new -socketpatch.<n> version) also removes the previous Socket module's sum
lines, so go mod tidy -diff stays clean after a patch update.
This requires a free, publicly retrievable patch reference carrying a goproxy
override. CLI support does not imply a patch is published for a particular module.
Paid hosted Go references are unsupported: embedding credentials in module paths
would expose them to module proxies and change module identity. Use vendored mode
for those patches; the CLI reports redirect_golang_unsupported when the required
hosted reference is absent.
Important limits:
- A replacement targets an exact original module version. Updating the
requirecan leave it unused; re-scan and regenerate VEX after dependency updates. - An internal
GOPROXYmust be able to serve or forward the Socket module path. Comma-separated proxy fallbacks do not recover from every HTTP error. - User-authored conflicting replacements are preserved and the patch is refused. Missing module metadata, untrusted Socket module paths, or missing integrity values are also refused before writing.
- Local directory replacements in agent and vendored mode are not checked against
go.sum; commit and review those trees. Hosted replacements use the module checksum mechanism above.
The implemented hosted shape and fresh-checkout behavior are exercised by
e2e_golang_hosted_build.rs.
Prebuilt binaries are published for:
| Platform | Architecture |
|---|---|
| macOS | ARM64 (Apple Silicon), x86_64 (Intel) |
| Linux | x86_64, ARM64, 32-bit ARM hard-float (arm-unknown-linux-gnueabihf / -musleabihf), i686 |
| Windows | x86_64, ARM64, i686 |
| Android | ARM64 |