All notable changes to socket-patch are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Pre-v3.0 entries are concise summaries derived from each tag's commit history. For full per-release detail, see the GitHub releases page.
The Release workflow refuses to publish a version that does not appear
in this file — see scripts/release-lint.sh (run by the version job in
.github/workflows/release.yml and by CI on version-bump PRs). Bump PRs
are opened by scripts/bump-version.sh, which rolls [Unreleased] over
into the new version's section — see docs/releasing.md.
Semver note: this entry changes
rollback's default behavior, narrows the meaning of its existingvendored: []JSON key, makes vendored mode manifest-free, moves vendored cargo wiring from.cargo/config*intoCargo.toml, tags vendored cargo copies' versions with+socket.<uuid>(visible to the patched crate asCARGO_PKG_VERSION), turns a plain non-TTYscanreport-only, and makesvexrefuse to attest stale ledger records and corrupt vendor ledgers — all MAJOR per CLI_CONTRACT.md's semver policy — so it ships as the next major release (v5.0).
- Vendored cargo copies carry a tagged version:
<version>+socket.<uuid>. The vendored copy's ownCargo.toml[package] versionis rewritten to the patch-tagged version (1.0.4+socket.<uuid>; a version that already has build metadata keeps it:2.0.1+zstd.1.5.2.socket.<uuid>), and the detachedCargo.lockentry records that tagged version with nosource/checksum— exactly the lock cargo itself writes when it resolves the[patch]against the tagged copy (verified by building on cargo 1.41 in docker and on current stable, and by the CIcargo-old-toolchainsleg on the 1.41 / 1.56 docker images — a local run without those images only type-checks on rustup toolchains:--lockedbuilds, the patched bytes compile, a registry crate that depends on the patched one (^1) resolves to the copy too, since cargo ignores build metadata when matching requirements, andcargo metadatareports the tagged version). Every lock reference that spells the old version ("cfg-if 1.0.4", v1's"cfg-if 1.0.4 (registry+…)") is rewritten to the tagged version, in lock formats v1–v4; a lock the edit cannot keep consistent (a leftover reference in another spelling, a v1replace, an entry already at the tagged version) refuses before any write withcargo_lock_untaggable, and a copy manifest whose version literal cannot be rewritten byte-exactly fails the package withcargo_copy_untaggable. The patch uuid of the copy cargo actually builds is therefore recoverable fromCargo.lockalone (a config-level[patch]override pointing elsewhere changes the locked version). The patched crate sees the tag inCARGO_PKG_VERSION(and inenv!("CARGO_PKG_VERSION")-derived strings such as--versionoutput of a vendored binary crate): requirement matching on it (semver::VersionReq::matches) is unaffected, but string comparisons AND equality / ordering on a parsedsemver::Versionsee the tag (semver1.x compares build metadata:1.0.4+socket.<uuid>is not== Version::new(1, 0, 4)and sorts above it). Re-runs are idempotent (a dry run previews the tag as "would tag", and the wet run'scargo_lock_untaggable/cargo_copy_untaggablerefusals); a uuid bump re-tags the copy and the lock;vendor --revert/rollback/remove/ GC / the hosted takeover restore the original lock byte for byte (tag dropped with thesource/checksum; a crate vendored before anyCargo.lockexisted has no originals, so only the tag its first build locked is dropped). A user's own same-version path crate that cargo later locks beside the tagged copy (an untagged sourceless entry) is never mistaken for it: re-runs stay in sync, and the revert restores the registry entry — spelled by its full id while the fork shares its name+version, exactly as cargo writes it — without touching the fork. GC keeps an entry whose lock tag is stale (another uuid) while the manifest still wires this entry's copy: cargo re-locks any unlocked build to the wired copy, and the next re-run retags. Projects vendored before tagged versions (the pre-v5.cargo/config*wiring, or an untagged manifest wiring) are tagged by the next re-run orrepair(cargo_version_taggednote; a tagrepaircannot write is thecargo_version_untaggedwarning); a whole-tree file inventory recorded for the copy is kept, and still verifies through exactly this uuid's tag, so tagging never re-baselines it over unverified bytes. VEX discovery treats the tagged lock version as the primary identity (pkg:cargo/<name>@<version>is the tag stripped): a detached entry tagged for a different uuid than the wiring's copy path is dead wiring (not attested), and so is a copy whose ownCargo.tomlis tagged for another uuid than its path; a tagged entry no visible wiring names is diagnosed unattributable; an untagged detached entry counts only beside an untagged copy (the pre-tag vendored shape) — beside a tagged copy it is some other crate cargo built, not attested. A patch that edits the crate's ownCargo.tomlverifies with the tag dropped — the tag being this copy's own uuid, the same pin the inventory check applies, so a copy tagged for another patch stays a mismatch instead of verifying clean while VEX refuses it. - Vendored cargo wiring moved to
Cargo.toml.vendor/scan/get --mode vendoredwrite the[patch.crates-io]path entry into the workspace-rootCargo.toml(beside theCargo.lockit detaches) instead of.cargo/config.toml/.cargo/config, so Socket scanners can recover the patch uuid from the manifest alone and single-version wiring builds on cargo older than 1.56 (the floor of config-file[patch]; proven on cargo 1.41 with no network). TWO vendored versions of one crate need cargo 1.45 or newer: from 1.45--offlinefrom an empty$CARGO_HOMEis enough (without--offlineit first tries to update the crates.io index and fails when that is unreachable), while cargo before 1.45 resolves every source-less lock entry for a crate through one[patch]path and fails closed on the other — see thecargo_multi_version_old_cargoentry under Fixed. The edit is format-preserving (comments, ordering, CRLF / mixed line endings, a UTF-8 BOM and the trailing-newline state survive; a revert restores the manifest byte for byte and keeps a user's own[patch]/[patch.crates-io]headers). The key is always the Socket-owned<name>-socket-<uuid8>withpackage = "<name>", never the bare crate name: cargo lets a config-file[patch]item (project, ancestor directory or$CARGO_HOME) replace the manifest item with the same key whatever its version, so a crate-named key could be silently shadowed — and two vendored versions of one crate get distinct keys instead of clobbering each other (the config wiring keyed by crate name let the second overwrite the first). The ledger'scargo_patch_entryrecord now namesCargo.toml(itskeyis the TOML key). New refusals, each before any write:cargo_manifest_unreadable,cargo_manifest_unparseable,cargo_manifest_symlink_unsupported(vendor and revert),cargo_manifest_not_workspace_root(run from a workspace member, whose[patch]cargo ignores) andcargo_manifest_patch_source_alias(the manifest spells crates.io by URL in[patch."https://github.com/rust-lang/crates.io-index"], which replaces[patch.crates-io]wholesale);user_authored_patch_entrynow covers user entries inCargo.tomland in every cargo config file cargo merges (project, ancestors,$CARGO_HOME) and matches by crate (packageor key), sparing a path patch that is provably another version. Old wiring migrates automatically: a re-run orrepairmoves a Socket-owned.cargo/config*entry intoCargo.toml(cargo_wiring_migratednote; a migrating vendor re-run reports the packageapplied) and cleans a config file /.cargo/the move emptied — a legacy entry that cannot be removed fails the run and unwinds it (cargo_legacy_wiring_kept) — whilerollback/remove/vendor --revert/ GC / hosted takeover remove both spellings. Projects hit by the pre-v5 multi-version overwrite (a detached lock entry nothing wired) are healed by a re-run orrepair(cargo_wiring_restored). User config entries are never touched. VEX discovery reads the manifest first (key-agnostic), skips a manifest entry that cargo ignores (a same-key project-config item or a URL-spelled crates.io table replaces it), and still honors pre-v5 config wiring. The hosted takeover's missing-ledger guard is now per version. - Binary Bun lockfiles are patched natively in place. Hosted and vendored
modes read and rewrite
bun.lockbformats 1–3 directly, including mode changes, repair, and scoped rollback. Binary-to-text conversion, migration ledger replay, and their warning codes and tests have been removed. rollbackis now the full-state dual ofscan.scanandrollbackare the batch primaries (get↔removestay the single-patch duals): a barerollbackrestores the SYSTEM to unpatched across all three modes — in-place file restore (agent), vendored unwire + artifact deletion + ledger-entry drop, hosted lockfile-redirect unwind + redirect-record drop — then removes the rolled-back entries from.socket/manifest.jsonand GCs the now-unused blobs plus diff/package archives. No--modeneeded: state is inferred from the manifest, the vendor ledger, and the redirect ledger, and rollback now runs manifest-less when a ledger holds work (hosted-only and vendored projects; the truly-empty project keeps the "Manifest not found" exit 1, and a wired-but-ledgerless project errors namingsocket-patch repair). Wet non-preserve runs confirm once ("Roll back N patches, remove them from the local manifest, and delete M vendored artifacts and their ledger records?" — auto-accepted under--yes/--json/non-TTY; declining prints "Rollback cancelled." and exits 0). Drift-keeps, hosted refusals/unsupported targets, corrupt ledgers, and a failed manifest write exit 1partial_failure; not-installed entries still exit 0.rollback --json'svendored: []array narrows to vendor-owned purls the run did NOT act on (today: the corrupt-vendor-ledger skip). Acted-on entries move to the new always-presentvendoredReverted/vendoredPreserved/vendoredKeptarrays; the envelope also gains always-presentwarnings[]({code, detail}, now populated),hosted({reverted, failed, unsupported, editedFiles}),manifest({removedEntries, preserved}),gc, andpathskeys.- Vendored mode is manifest-free.
scan --mode vendoredandget --mode vendorednever write (or read).socket/manifest.json: the selected patch records are fetched into memory and every vendor-ledger entry carriesdetached: trueplus the embeddedrecordas its verification source, so a vendored project's footprint is.socket/vendor/**only. The former--detachedopt-in is now the only vendored posture — the flag is hidden, accepted as a no-op for compatibility, and still a usage error without vendored mode. JSON uses the detached download vocabulary for both commands (downloaded: N,detached: true,patches[].action=downloaded|skipped|failed). The vendor step vendors exactly what discovery selected — the "whole manifest is vendored" re-vendor from a committed manifest on an empty discovery is retired (repairverifies and rebuilds committed vendored state) — and a legacy manifest record for a purl a vendored run vendors is migrated into the ledger (dropped from the manifest; an emptied manifest is left as{"patches": {}}).listnow reads the vendor ledger too, so a vendored-only project lists its patches with aMode: vendoredlabel and exits 0 instead ofmanifest_not_found;scan --prune's lockfile-unused reconcile applies to every ledger entry (the check is about the lockfile, not the manifest); and standalonevendorwith no manifest is a clean exit-0 no-op whose message names the missing manifest (and the ledger entriesrepairverifies) instead of claiming "No .socket folder found". - A plain
scanwithout a TTY is report-only. When stdin is not a TTY,--yesis absent, and no intent flag (--mode,--apply,--sync,--vendor,--redirect,--prune) is given, human-modescanprints the discovery report and the "To apply a single patch, run: …" hint, downloads nothing, creates no.socket/, and exits 0 — it no longer auto-accepts the apply prompt. Any intent flag,--yes, or a TTY keeps the previous behavior;rollback/remove/get's non-TTY auto-accept is unchanged. Humanscan --mode hostednow prints the results table and update detection like the other modes and confirms once ("Redirect N packages to the hosted patch server?" — the same prompt asget --mode hosted— default yes, skipped by--yes/--json/--dry-run; on a non-TTY stdin without--yesit printsNon-interactive mode detected, proceeding automatically.and proceeds), fetches patch details with the agent arm's progress counter and per-package warnings, and an empty hosted discovery printsNo patches available for installed packages.and exits 0 without entering the redirect engine (wasRedirected 0 package(s)); a discovery whose every offer is paid-tier for an org without paid access stops the same way withNo downloadable patches (paid subscription required).. A malformed redirect ledger on a human hosted run that stops before the engine is reported as the read-onlyWarning: the redirect ledger … is malformedadvisory instead of nowhere. apply.locknever outlives a command, and hosted mode takes it. Lock acquisition creates.socket/when missing; the lock file is unlinked (while still held) and an otherwise-empty.socket/removed when the command exits, dry runs included, so there is nothing to.gitignoreandrepairno longer has a lock-cleanup step (a leftover from a crashed run is reclaimed and removed by the next lock-taking command; a live holder is stilllock_held, exit 1).scan/get --mode hostednow acquire the lock around their first wet write — never on--dry-runor when nothing would be written, so previews create no.socket/— and reportlock_held/lock_iolike the other lock holders (top-levelerrorCodeon the hosted JSON shape; a read-only project root or a file squatting on.socket/is refused at the lock, before the redirect ledger is touched, and a vendored→hosted takeover over a symlinked wiring file is refused withredirect_symlinked_file_unsupportedbefore any revert). A zero-grant wet run — which holds no lock — no longer moves a malformedredirect-state.jsonaside: like a dry run it reports the hard error and leaves the file in place; only the lock holder quarantines. The lock guard unlinks only the file it holds (a replacement planted by a non-cooperatingrm+touchis left for the next acquire), and a long--lock-timeoutwait behind a hot loop of short commands can no longer accumulate its vanished-file retries into a spuriouslock_io. Agent-modegetandscan --apply/--synchold one lock window across download → manifest write → nested apply (the nested apply no longer re-acquires and now inherits--lock-timeout/--verbose);setuptakes the lock while persisting--exclude;scan --pruneacquires once for its vendored reconcile and manifest prune, and the GC legs ofscan --pruneandvendorhonor--lock-timeoutand report a lock I/O error instead of silently skipping on it.- Retired: the legacy
.socket/cargo-patchesredirect takeover in the cargo vendor backend (never shipped in a tagged release — such[patch.crates-io]entries now refuse asuser_authored_patch_entry) and thepypi_pipenv_invalid_wheelrefusal code (the Pipenv backend takes the resolved version instead of parsing the wheel filename). vexattests a ledger record only while a lockfile still wires it — even under--no-verify. A vendor-ledger entry whose artifact no lockfile/config references any more is omitted asvendor_unwired, and a redirect-ledger record whose hosted patch no lockfile references asredirect_unwired(a manifest-owned purl falls back to agent-mode verification instead). Previously a reverted lockfile plus a leftover.socket/vendor/state.json/redirect-state.jsonkept attesting, and--no-verify/--vex-no-verifyattested every ledger record outright; those flags now skip only the hashing, never the wiring, record-match and conflict gates. A malformed or unreadable.socket/vendor/state.jsonis now the hard errorvendor_ledger_corrupt(exit 2 standalone, the host command fails under--vex), mirroringredirect_ledger_corrupt, instead of a warning after which vendored patches silently lost their committed-artifact verification and detached records. Lockfiles that wire one package to different patches attest none of them (wiring_conflict), and when the lockfile wires a package to patch U, a manifest or ledger record for it under another uuid is superseded.
-
redirect_yarn_berry_mixed_line_endingsandvendor_yarn_berry_mixed_line_endings. Ayarn.lock(or, vendored, a rootpackage.json) that mixes CRLF and LF line endings — or holds a bare CR — has no single ending to keep, and yarn itself rejects such a lock under--immutable(YN0028) and rewrites it wholesale on its next plain install. Both modes now refuse it before any write with a code naming the line endings and theyarn installremedy; a revert never refuses on line endings (a restored lock entry takes the terminator of the entry it replaces). The real-yarn berry suites gainedSOCKET_PATCH_YARN_BERRY_EOL=crlfto run on CRLF files on macOS / Linux as yarn writes them on Windows, and print aBERRY-EOL|<yarn>|<flow>|<file>|yarn=…|flow=…line per fixture file — see yarn berry compatibility. -
Hosted npm redirects configure npm 12's
allow-remotefor you. npm 12 defaults toallow-remote=noneand refuses (EALLOWREMOTE) a lock that resolves patched packages from the Socket patch host. Whenscan --mode hosted/get --mode hostedleaves a rootpackage-lock.json/npm-shrinkwrap.jsonredirected, it now writesallow-remote=allto the project.npmrc— creating the file, or appending one line with the BOM, CRLF and every other byte preserved — so a plainnpm ciinstalls the patched bytes on npm 12 (verified on npm 10.9.9, 11.20.0 and 12.1.0). The edit is ledger-recorded (redirect_npmrc_allow_remote,created/added) androllback,remove, scoped unwinds and the hosted → vendored takeover remove exactly what was added once no package-lock entry needs it (a modified created file keeps its other lines:redirect_npmrc_allow_remote_modified, reported byrollback,remove,vendorand the vendored reconcile). An explicit userallow-remote=none/rootis respected — in the project.npmrc, in the user / global / builtin npm config (a committed project line would silently override that machine policy), or in annpm_config_allow_remoteenvironment variable (which beats every.npmrc) — and the warning names where it was found. A symlinked, unreadable or bare-CR.npmrcis left alone (and a symlinked one refuses an unwind before anything is written),--dry-runwrites nothing but previews the write — also for a vendored → hosted takeover — and--no-npm-allow-remote-config/SOCKET_NO_NPM_ALLOW_REMOTE_CONFIGopts out.redirect_npm_allow_remotenow fires on EVERY hosted npm run — including when.npmrcalready allows it — and always states the tradeoff:allow-remote=alladmits any url-resolved dependency, not just Socket's, while the sha512 integrity pins stay enforced. The.npmrcsniff now follows npm 12's measured grammar: only the exact keyallow-remotecounts (anallow_remote/ALLOW-REMOTEline, which npm ignores, previously silenced the warning), lines split on a bare\ras well as\n, a[section]header only counts on the untrimmed line (as in npm'sini), section bodies are not top-level (a section-scoped copy of the line never makes the unwind ambiguous), and the value is case-sensitive. Mode-preserving atomic writes now create their stage file with the destination's permission bits, so a 0600 token-bearing.npmrcis never staged world-readable. Vendored mode is unaffected (npm gatesfile:tarballs byallow-file, defaultall). -
Manifest-less VEX:
vexattests hosted and vendored patches straight from the lockfiles.socket-patch vex, andapply/scan/vendor --vex, no longer need.socket/manifest.json, or the.socket/vendorledgers, to attest hosted and vendored patches. That covers a depscan-opened PR, a clone that never committed its ledgers, and a lock-only CI checkout. New read-only discovery (socket-patch-corevex::discover) reads every supported root lockfile and config:- npm: package-lock / shrinkwrap;
- pnpm: every lock generation, plus Rush locks;
- yarn classic and berry;
bun.lock/bun.lockb;- cargo:
Cargo.lock+Cargo.toml+ cargo config; - Go:
go.mod/go.work+ sums; - Python: uv, PEP 723 script locks,
pylock.toml, poetry, pdm,Pipfile.lock, requirements (+-rincludes), Hatch / PEP 621 direct references; Gemfile.lock/gems.locked;composer.lock;- maven:
pom.xml; - nuget:
nuget.config+packages.lock.json.
Each patch uuid is recovered from a Socket patch-host URL (or the
--patch-server-urlorigin) or a.socket/vendor/<eco>/<uuid>/…path, validated fail-closed. A lock that resolves the same package elsewhere contests the wiring and blocks it. Records come from the manifest, then the ledgers, then (online only) the patch API by uuid; nothing is written to the manifest.--offlineor a failed fetch omits the patch asrecord_unavailable, and a record naming another patch or package asrecord_mismatch. Evidence: vendored patches hash the committed artifact. Hosted patches hash the installed copy the build consumes (the Go replacement module, never the pristine cache copy), or, before any install, attest from the lockfile's integrity pin. Unreadable or unparseable lockfiles and rejected references surface as run warnings (lockfile_unreadable,lockfile_unparseable,patched_ref_invalid,patched_ref_unattributable) and never abort the run. Why a patch was gated rideswarnings[]too, so--jsonkeeps it: a failed record fetch (vex_record_fetch_failed,vex_record_not_found,vex_record_offline), a stale-credential fallback to the public proxy (api_auth_fallback,get/scan's warning), a wiring conflict (vex_wiring_conflict), a superseded record (vex_record_superseded) and a dead ledger claim (vex_claim_unwired); a failed embedded--vexadds onevex_omittedper omitted patch to the host command'swarnings[].apply --vexandvendor --vexwith no manifest now write the document instead of exiting 0 with none. A project with nothing wired anywhere keeps that calm exit, andapply --checknever generates. Manifest-less runs honorvex --dry-runand-O -like everyvexrun, and show a transientFetching patch records...status line on a terminal. Per-PM support and limitations: the README's "No manifest needed for hosted and vendored patches" and CLI_CONTRACT.md's "Manifest-less VEX". -
One lockfile reader per format, and one ledger-liveness rule. Discovery and the lock inventory read each lockfile through one reader (the package-lock, composer.lock and Pipfile.lock entry walks, the hosted rewriter's
pnpm-lock.yamlgrammar with one key grammar for every lock generation, the backends' fail-closedbun.lockline grammar — so a hand re-indentedbun.lockis diagnosedlockfile_unparseableinstead of attested — one berry key splitter, which the vendored berry backend now uses too, a sharedGemfile.lockmodel, the vendor backend'sCargo.lockmodel, the Python lock and requirements-file readers the rewriters use, with one exact-pin rule and one hosted pypi url grammar).scan's cross-mode takeover warnings (redirect_supersedes_vendored,vendor_supersedes_redirect),hosted_wiring_retainedandredirectState.wiringLivenow prove the live lock with the same discovery and liveness rulesvexgates attestations on, instead of a looser text scan: a ledger record counts as live only while a lockfile wires that package to the record's patch (a hosted url carrying its uuid, or the exact vendored artifact the ledger names). That rule reads EVERY lock's entry for the package (a PEP 723 script lock resolving it from PyPI no longer vetoes the project lock's hosted entry) and a recorded Gemfile with discovery's source-block grammar (a commented-outsource … doblock no longer keeps a reverted record alive). The shared readers also change the lock inventory at the edges: a v1Cargo.lock's[metadata]checksums now verify a crates.io fetch,requirements.txtis read as pip's logical lines (continuations joined, comments cut, a BOM dropped), a wildcardrequirements.txtpin (six==1.*) is no exact version, a Pipfile.lock hosted reference to an sdist, anhttp://origin or a path-prefixed origin stays inventoried, a CRLFpnpm-lock.yamlis inventoried (it used to read as having no packages), and apoetry.lock/pdm.lock/Cargo.lockthat is not valid TOML contributes nothing instead of a best-effort line scan. The vendored berry backend splits a multi-descriptor lock key the way yarn writes it, so a key mixing the patched package with another descriptor ("lp@npm:left-pad@1.3.0, left-pad@npm:1.3.0") is refusedvendor_override_conflictinstead of being read as one descriptor. -
Standalone
vendorembeds the patch record in its ledger entries too. Vendored mode (scan/get --mode vendored) already writes every entrydetached: truewith its embeddedrecord; the manifest-drivenvendorcommand now also writesrecordinto.socket/vendor/state.json(neverdetached— the manifest record stays authoritative while the manifest covers the package), so a project it wired whose manifest is gone still verifies, lists and attests offline:vex,listandsetup --checkread the embedded copy whenever no manifest entry covers the package, andrepairrecovers the record without the API when there is no manifest at all. Entries written by older releases keep working. -
VEX product auto-detection covers Go, Composer, Maven, NuGet and RubyGems projects, after the existing git /
package.json/pyproject.toml/Cargo.tomlprobes:go.modmodule,composer.jsonname,pom.xmlcoordinates, the root's single*.csproj, the root's single*.gemspec. Ambiguous roots yield no product rather than a guess. -
Pipenv projects can use hosted patches, and vendored patches keep every category.
scan --mode hostedrewrites everyPipfile.lockcategory (default,develop, Pipenv 2022+ named categories) that pins the patched release to the hosted wheel —filereferences for Pipenv 2018 and later,pathfor 7–11 (probed once withpipenv --version;SOCKET_PIPENV_MAJORpins it), pipfile-spec < 6 refused — preserving markers, extras, unrelated entries, the Pipfile and its content hash, with per-entry rollback (redirect_pipenv_entry). Vendored mode keeps custom categories and extras, usespathfor wheels with extras (Pipenv 2022's file-URL bug) and refuses installers older than 2018 (pypi_pipenv_installer_unsupported). A stale Pipfile.lock only vetoes the sibling Python rewriters on a real pin/source conflict (redirect_pipenv_refused); anything else isredirect_pipenv_skipped. Measured across the last stable release of all 18 published Pipenv majors — seedocs/testing/pipenv-compatibility.mdandscripts/backtest-pipenv.py. -
Pipfile.lockis inventoried. Lock-only Pipenv checkouts (a fresh clone with nothing installed) now discover their pins in every mode — hosted redirects them, vendored fetches the pristine wheel by one of the lock's recorded digests (LockIntegrity::Sha256AnyOf, resolved through PyPI's JSON API and verified against the same digest) and agent/scan list them as lockfile-only packages. Previously they discovered nothing and exited 0. Socket's own references stay discoverable, so a re-scan of an already-redirected or already-vendored lock-only checkout re-confirms it; a lock that resolves only from private indexes is never looked up on pypi.org. -
Pipenv's out-of-tree virtualenv is discovered. Agent mode (bare
scan,rollback,vex) now finds$WORKON_HOME/<dir>-<hash>[-<python>](the.venvfile pointer,PIPENV_CUSTOM_VENV_NAMEandPIPENV_PIPFILEincluded) exactly as Pipenv 7 through 2026 place it, instead of falling through to the global interpreter's site-packages. -
Pipenv stale-install guard. Pipenv never reinstalls a release that is already present, so a hosted or vendored rewrite over a warm venv leaves the upstream bytes installed;
redirect_pypi_stale_install/pypi_pipenv_stale_installnow say so, naming the site-packages dir and the verified remedy (pipenv run pip uninstall -y <pkg> && pipenv sync, or a cleanpipenv --rm && pipenv sync), and the stale purl is excluded from the same-run--vex. -
PDM projects take hosted patches, and hosted and vendored patches share one validated
pdm.lockrewriter.scan --mode hostedrewritespdm.lockto point the target[[package]]at the hosted wheel URL with the patched SHA-256 (redirect_pdm_lock_package), andscan --mode vendoredwires the same unit to a committed wheel through the shared rewriter (utils/pdm_lock.rs). Both preserve line endings and non-canonical spacing, support the legacy[metadata.files]table and separateextrasentries, are idempotent, and leavepyproject.tomlandcontent_hashuntouched. Supported lock formats are2(PDM 0.12–1.4) and4.3–4.5.1(PDM 2.8.1+; PDM 2.8.0 writes the same4.3lock but still loses candidate identity, so upgrade to ≥ 2.8.1); the identity-losing3.1/4.0–4.2formats (PDM 1.8–2.7) and unknown future formats are refused before any write (redirect_pdm_refused/pypi_pdm_lock_version_unsupported), leaving the registry lock installable. Apdm.lockwritten by PDM 0.x/1.x (lock format2) warnsredirect_pdm_legacy_sync_required: those releases have an upstream freshness bug, sopdm installcan regenerate the lock — usepdm sync. Verified end-to-end against real PDM 0.12–2.29 across hosted, vendored and agent mode on Linux, Windows and macOS; seedocs/testing/pdm-compatibility.mdandscripts/backtest-pdm.py. When several Python lockfiles coexist,uv.lockandpoetry.lockdrive hosted PyPI redirects ahead ofpdm.lock, so a leftoverpdm.lockbeside them neither blocks a live redirect nor is falsely attested. A hosted lock-onlypdm.lockcheckout is discovered and redirected (the lock inventory now readspdm.lock), and a re-scan after an externalpdm lockrebases the ledger's recorded edits onto the relocked text sorollbackstays byte-invertible even when PDM reflows the lock's line endings. -
Poetry projects take hosted patches, and vendored patches now cover every
poetry.lockgeneration.scan --mode hostedrewritespoetry.lockto a[package.source] type = "url"pointing at the Socket-hosted, SHA-256-pinned wheel (Poetry 1.0 through 2.x; Poetry 0.12 ignores URL sources and is refused withredirect_poetry_lock_unsupported), andscan --mode vendoredaccepts the legacy[metadata.hashes](0.12) and[metadata.files](lock 1.0/1.1) layouts next to the 2.xfilesarrays, CRLF locks included. The rewrite keeps every other byte of the lock — dependency metadata, groups, markers, extras and the pyprojectcontent-hash— and records independent rollback fragments per patch, sorollbackrestores the recorded originals in any order. Verified end-to-end against real Poetry 0.12.17, 1.0.10, 1.1.15, 1.2.2, 1.3.2, 1.4.2, 1.5.1, 1.6.1, 1.7.1, 1.8.5, 2.0.1, 2.1.4, 2.2.1, 2.3.4 and 2.4.3 in hosted, vendored and agent mode — seedocs/testing/poetry-compatibility.mdandscripts/backtest-poetry.py. Poetry releases before 1.4 neither verify local wheel hashes nor replace an already-installed package at the same version; both modes surface that as an advisory (pypi_poetry_integrity_unverified,redirect_poetry_stale_install_risk) keyed on the lock's writer, and the hosted rewriter warnsredirect_poetry_entry_not_foundwhen a lock has no entry for a granted patch (uv parity). A rotated grant token or republished patch supersedes the earlier hosted URL in place instead of being refused as a foreign source, a futurelock-version = "2.<n>"is rewritten like 2.1 on every path (the vendored loader already accepted it), and a malformed[metadata.files]/[metadata.hashes]value is refused instead of panicking the scan. Rollback stays invertible across Poetry's own relocks: the recorded package fragment carries its boundary header, so a unit that Poetry 1.1/1.2 re-laid (source kept, insertedfilesline dropped) is refused rather than mistaken for an already-reverted lock, a lock-1.0 redirect restored by hand converges instead of refusing, and a re-scan after such a relock REBASES the ledger's edits (pristine → current) instead of appending a chain whose older links match nothing — which maderollbackandremoverefuse forever. (#241) -
Python patches survive uv lockfiles in both hosted and vendored modes.
scan --mode hosted|vendorednow rewrites nativeuv.locktogether with the pairedpyproject.tomlsource and metadata, PEP 723 script locks (*.py.lockplus the script's inline metadata), PEP 751pylock*.toml, and uv-compiled hashedrequirements.txt, souv sync --frozen|--locked,uv run --script, anduv pip sync --require-hashesinstall the patched wheel instead of the registry artifact. Verified against real uv binaries from every 0.x release family (0.0 through 0.12) — first and latest release of each plus every observed behaviour boundary — seedocs/testing/uv-compatibility.md: hosted mode covers requirements from uv 0.0.5 and nativeuv.lockfrom 0.1.45 — the first release whoseuv lockwrites one — through all three[[distribution]]lock shapes and[[package]]; vendored native covers every[[package]]release (uv ≥ 0.2.35), vendored requirements cover uv ≥ 0.1.24 (hash-enforced byuv pip sync --require-hashesfrom 0.1.32 and by default from 0.5.x). Follow-up hardening:vendor --revertrefuses to delete a vendored Python wheel a lock still references when the ledger entry has no wiring to replay (the shaperepairrebuilds), a script or PEP 751 lock supplements rather than hidespoetry.lock/requirements.txtpins, symlinked locks are discovered, and refused before any write (never rewritten in place — uv writes through the link, an atomic rename would replace it; hostedredirect_symlinked_file_unsupported, vendoredpypi_uv_symlink_unsupported/pypi_lock_symlink_unsupported), CRLF locks keep their line endings in both the hosted rewriter and the vendored uv backend (pyproject.toml, the rewritten[[package]]unit and the appended[manifest]/[package.metadata]fragments, plus their revert), and the hosted[tool.uv.sources]edit renders as a header after[project]the way uv writes it. (#238, #239) -
uv
--lockedsurvives the project shapes the plain fixture never reached. A package listed only in[tool.uv] dev-dependenciesis now classified as a direct dependency (it was wired as a transitive override and itsrequires-deventry left stale), and every duplicaterequires-dist/requires-deventry for the package — extras, markers — is repointed rather than only the first, souv sync --lockedanduv lock --checkaccept the patched lock instead of exiting 2 and a plainuv syncno longer rewrites it.[tool.uv] constraint-dependencies/build-constraint-dependenciesnaming the package have their[manifest]constraints/build-constraintsentries repointed too (uv ≥ 0.5.6 serializes them with the package's source; 0.2.37–0.5.3 reject the repointed entry under--locked, so the repoint emits the advisorypypi_uv_constraints_require_uv_0_5_6). The transitive (override-dependencies) branch emits the advisorypypi_uv_override_requires_uv_0_5_6: uv applies[tool.uv.sources]to overrides only from 0.5.6, so on 0.2.35–0.5.3--frozeninstalls the patch but a plainuv syncreinstalls the registry wheel. The[[distribution]]-grammar vendoring refusal now names the real reasons (relative path sources unparseable through 0.2.6, rejected by--lockedand absolutized byuv lock/uv syncon 0.2.17–0.2.34) instead of "records absolute paths".scripts/backtest-uv.pygains a project-variant lane covering these shapes on every[[package]]binary and a--render-doc-tablemode that prints the doc's results tables fromresults.json; its export lane readsuv exportfrom stdout because--output-fileonly exists from 0.4.7. (#239) -
Unwired Python vendor entries revert safely.
vendor --revert/rollbackon a ledger entry without wiring to replay (the shaperepairreconstructs) skips the lock-reference guard under--preserve-state(nothing is deleted, so nothing needs protecting), refuses fail-closed when the project root cannot be listed or a lock's symlink target cannot be read (instead of treating "could not enumerate locks" as "no lock references it" and deleting the wheel), probes-r/--requirementincludes ofrequirements.txtalongside the root file — the orphan sweep andrepairsee include-hosted pins too — and reclaims a genuinely orphaned artifact directory whose lock is gone or whose ledger flavor is unknown instead of failing forever. Hostedscan/get,repair, and ledger-lessrollbackread candidate lockfiles through the FIFO-safe reader, so a named pipe at a lockfile name no longer wedges the command inopen(2). (#239) -
Path targeting on
scanandrollback.scan [PATHS]...scopes discovery to packages with an installed copy under a matching glob (ancestor rule:scan packages/foocovers the subtree;*never crosses/; absolute patterns are the only way to reach--globalstores); the prune universe is never narrowed (scan PATHS --pruneprunes exactly what an unscoped run would), lockfile-only/vendor-ledger supplements are excluded with apath_scope_excluded_supplementswarning, an empty match is a normal empty scan (exit 0, no GC), and PATHS is rejected with--mode hosted|vendored(exit 2).rollback [TARGET]...accepts PURLs, UUIDs, and path globs (variadic, unioned); only path-SHAPED tokens (separator, glob metachar,./prefix, absolute) become globs, so a mistyped identifier stays a safe exit-1 error. A path target selecting nothing is an error on rollback (exit 1) and an empty scan on scan (exit 0); path targets select installed copies, and rollback restores EVERY installed copy of a selected patch (out_of_scope_copies_restoredwarning when copies live outside the patterns). -
--preserve-stateonrollbackandremove(envSOCKET_PRESERVE_STATE): fully unpatch the system but keep the local state for a later re-apply — manifest entries, vendored artifacts + ledger entries (kept byte-identical; re-vendor re-wires from the live lock) — and skip all GC. Hosted redirects have no preservable state: they are unwound and their records dropped either way (hosted_state_not_preservablewarning). Onremove, combining it with--skip-rollbackis a usage error (exit 2, flag- or env-sourced): the combination would be a no-op — one flag keeps the tree and drops the state, the other restores the tree and keeps the state. -
Hosted redirect unwind. Per-purl reverts for cargo + the npm family, plus a whole-ledger reverse replay (core
patch/redirect/replay.rs) that runs whenever the scope covers every redirect record: a per-kind inverse table, staged all-or-nothing per ecosystem group, covering gem, golang, pypi, composer, bun, and the non-package rideshare edits (pnpmtrustLockfileauto-config — pristine scaffold deleted, modified scaffold keeps the file and loses only the owned line). Nativebun.lockbpackage snapshots restore binary resolutions directly; maven and nuget fail closed withhosted_revert_unsupportedguidance (their structured-metadata edits keep their ledger records; re-runscan --mode hostedor restore from VCS). Refused groups keep their edits AND records — the coherent ledger a retry needs. -
removegains the hosted leg and full archive GC: an identifier matching hosted redirect-ledger records unwinds those redirects (per-purl or via the replay when it covers the full record set; works manifest-less on hosted-only projects; unsupported ecosystems fail closed withhosted_revert_unsupportedbefore the manifest mutation), and remove's default GC extends from blobs-only to blobs + diff + package archives (parity with rollback/repair/scan --prune).
-
Hosted Go redirects no longer claim patches that did not land.
scan/get --mode hostedcounted a Go module as redirected (recorded it in the redirect ledger, sovexattested it) whenever any project file contained the patch-server origin — e.g. an already hostedpackage-lock.json— or leftoverpatch.socket.dev/gopatch/…go.sum lines, even when the go.mod rewrite was refused. A Go module now counts only when its go.modreplaceand both go.sum lines are in place. A module that go.mod does not require and go.sum does not list at the patched version (another project's module found in the shared module cache) is refused withredirect_golang_not_in_module_graphinstead of getting an inertreplace. -
Switching a vendored Go module to hosted mode cleans up the vendored copy.
scan/get --mode hostedrewrote the vendoredreplacebut left.socket/vendor/golang/<uuid>/and its vendor-ledger entry behind, so the nextvendorrun switched the module back. The hosted run now reverts the vendored state first, as it already did for cargo and npm. -
Go
replacedirectives the CLI did not write are left alone. A replace onto another checkout's vendored copy (../other/.socket/vendor/golang/…) or onto a module underpatch.socket.dev/gopatch/other than<patch uuid>was treated as socket-owned and could be rewritten or removed. Only./.socket/vendor/golang/…,./.socket/go-patches/…and exactlypatch.socket.dev/gopatch/<uuid>are now owned. -
Go replace edits keep go.mod valid and readable. A go.mod carrying two socket-owned
replacelines for one module (for example after a merge) is collapsed to one instead of leaving a duplicate go rejects; refreshing a directive keeps its trailing// comment; and quoted module paths ("github.com/x/y") are recognized, so a quoted user replace is no longer duplicated and a quotedrequirestill gets the version check. -
+incompatibleGo modules resolve in vendor and apply. Purls that spell the versionv2.0.0%2Bincompatibleare now percent-decoded, so the module is found in the module cache and thereplaceand copy directory carry the real+incompatibleversion. -
+incompatibleGo copies surviveapplyreconcile. A manifest key spelled%2Bincompatibleno longer makes the freshly applied.socket/go-patches/…@v2.0.0+incompatiblecopy look orphaned, so it and itsreplaceare kept. -
Hosted Go rollback restores go.sum byte for byte. The upstream module's go.sum lines that the redirect pruned went back at the end of the file; they now return to the position
go mod tidysorts them to (semver order within a module). CRLF go.mod/go.sum files also unwind cleanly, with no leftover socket lines or blank lines. -
Vendoring a hosted Go module unwinds the hosted redirect first.
vendor/get --mode vendoredover a hosted-redirected Go module left its redirect-ledger record and the socket module's go.sum lines behind (with the upstream lines still pruned). Go now takes the same per-purl takeover revert as cargo and npm (vendor_takeover_reverted_redirect), and scopedrollback <purl>/remove <purl>of one hosted Go module works without an unscoped rollback. -
Vendored Go fetches honor
GOPROXY=off,directandGOPRIVATE. With the module missing from the module cache, the pristine fetch fell back tohttps://proxy.golang.orgeven when go itself would ask no proxy, sending private module paths off the machine. It is now refused (vendor_fetch_unverifiable, then the usualpackage_not_installedskip) unlessSOCKET_GOPROXYnames a proxy. -
yarn berry projects on Windows (CRLF files) are redirected and vendored instead of refused. yarn berry (2.x–4.x) writes a file it creates with the OS line ending (
os.EOL) and keeps an existing file's majority ending on every later write (normalizeLineEndingsin yarnpkg-fslib'sFakeFS.ts, used byProject.persistLockfileandWorkspace.persistManifest) — so on Windows a freshyarn.lockand thepackage.jsonyarn first pretty-prints are CRLF, and acore.autocrlfcheckout makes them CRLF on any OS.scan --mode hosted/get --mode hostedrefused every such lock (redirect_yarn_berry_crlf_unsupported, redirected 0); a CRLF lock is now rewritten in its own line ending — every untouched byte, a leading BOM included, round-trips — and the ledger records the lock's on-disk CRLF fragments, sorollback,removeand the hosted → vendored takeover restore it byte-for-byte (they also replay a ledger recorded on a checkout whose uniform line ending has since flipped, LF ↔ CRLF).vendor/scan --mode vendorednow keeppackage.json's layout (BOM, indent, line ending, trailing-newline shape) on both the wiring and the revert:vendor --revertwrote a CRLF manifest back LF, never byte-identical to the pre-vendor file. A BOM'dpackage.json(and a BOM'd.yarnrc.yml, whose first-linecompressionLevelwas read as unset) no longer fails the vendored backend, and every berry reader skips a BOM in front of a header-less__metadata:. Verified on real yarn 4.12.0 (hosted, vendored, workspaces, pnpm linker, both mode takeovers) with the fixtures re-spelled CRLF, and on yarn 2.4.3 / 3.8.7 (still refused for their cacheKey, never for their endings). -
A yarn berry mode takeover no longer strips the old mode's patch before the new mode refuses the project.
scan/get --mode hostedover a vendored berry purl reverted its vendored wiring, ledger entry and artifact (redirect_takeover_reverted_vendored: "now fully hosted") and only then ran the rewriter, which refused a lock with mixed line endings (or an unsupportedcacheKey/.yarnrc.ymlcompressionLevel) —redirected: 0, and the nextyarn installpulled the unpatched registry package.vendor/scan --mode vendoredover a hosted berry purl did the same in reverse (vendor_takeover_reverted_redirect, thenfailedvendor_yarn_berry_mixed_line_endings). Both takeovers now run the new mode's berry gates first — wet and--dry-runalike — and a refused purl keeps the old mode's wiring byte-identical. -
setupkeeps a CRLFpackage.jsonCRLF.setupandsetup --removere-serializedpackage.jsonwith bare LF and dropped a leading BOM, so on a Windows yarn berry project (yarn pretty-prints the manifest with CRLF) a two-key script edit became a whole-file diff that yarn then kept, andsetup --removecould not land byte-identical on the pre-setup file.package.jsonis now written in its own layout (BOM, indent, line ending, trailing-newline shape), the same helper the vendored backends use. -
Two vendored versions of one cargo crate are documented — and now warned about — as needing cargo 1.45. The docs said older cargo (1.41) only needed a populated crates.io index. It needs more than that: cargo before 1.45 resolves every source-less
Cargo.lockentry for a crate through ONE[patch.crates-io]path — the entry whose KEY sorts last — so one of the two versions is pinned to the other's copy andcargo build --lockedfails closed withpatch for `<crate>` … did not resolve to any crates, index or no index. The old-toolchain e2e passed only because its fixture uuids happened to sort the other way; it now uses the adversarial order, and the floor was measured rather than assumed — on one two-version fixture in both key orders, 1.41.1, 1.42, 1.43 and 1.44 refuse the adversarial order while 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 warns withcargo_multi_version_old_cargounless the project'srust-versionorrust-toolchain[.toml]promises cargo 1.45 or newer (socket-patch never runscargo, so those files are the only signal it has). A SINGLE vendored version still builds on cargo 1.41, as before. -
A CRLF
Cargo.lockstays CRLF, and reverts byte-for-byte. Vendoring rewrote every line of a lock committed with Windows line endings as LF (toml_editrenders LF only), andvendor --revertthen "restored" the all-LF file — a whole-file diff on a Windows checkout and a rollback that was not byte-identical. The lock edits (detach, retag, restore) now map the rendering back onto the file's own line endings, the way the copy'sCargo.tomlalready did, in lock formats v1–v4: a CRLF lock stays CRLF, a missing trailing newline stays missing, and every line a mixed-ending lock's edit leaves alone keeps its own ending. -
Vendored mode can patch a package whose version carries build metadata. The patches API serves canonical PURLs, so a semver build metadata version arrives percent-encoded (
pkg:cargo/wasi@0.11.0%2Bwasi-snapshot-preview1). The PURL parsers compared that raw spelling against the lockfile / install directory's0.11.0+wasi-snapshot-preview1, never matched, and refused the package (vendor_fetched_missing, thenlocked_version_mismatch) — so no cargo crate with build metadata (wasiis in most Rust dependency graphs) could be vendored at all. Every ecosystem's PURL parse now percent-decodes the namespace, name and version once, after the/-and-@split and before the path-safety guards, so an escaped separator still cannot introduce a path segment. Hosted mode was already correct. -
A vendoring-service outage no longer re-vendors packages. An npm re-run (every lock flavor,
bun.lockbincluded) re-acquired its tarball from whichever source answered — the service's prebuilt, or a local pack with different bytes — so an outage or its recovery rewrote the lock's integrity and the committed tarball and reportedapplied. A re-run now keeps the committed artifact whenever the vendor ledger vouches for it (uuid-bound path, no symlink, sha256 + size equal to the ledger, a canonical archive an installer extracts exactly as decoded, every patched file verified from the same bytes) and isalready_vendoredwith no service request, in every--vendor-sourcemode (includingservice+--offline, as cargo and composer already did; golang now matches). A pypi re-scan after a relock re-wires the committed wheel instead of pinning a new sha, the PDM partial-relock guard holds whichever source built the wheel, a wiring failure no longer deletes a committed wheel, and a missing prebuilt wheel during an outage now says to wait for the service.--dry-runpreviews the same reuse, so it no longer predicts aservice+--offlinerefusal the real run does not have. Transient service failures (network, timeouts, 429, 5xx) are retried with backoff, each attempt is time-bounded, and after two consecutive exhausted fetches the run stops calling the service. -
Terminal output is clean on every command. Progress lines no longer leave stale text behind (
scanprinted e.g.Found 7 patches for 1 packagesatch 7/7)) or run into warnings printed while they are active. Progress, prompts, color and truncation now share one implementation.- Progress lines: a status line clears itself on finish. It is never
drawn off a TTY, under
TERM=dumb, in debug mode, or under--json/--silent.fetch,vendor,setup, lock waits and--updatechecks now show progress instead of going quiet. - Prompts: Ctrl-D at a
[Y/n]prompt now declines instead of accepting. Keys pressed while a scan is running no longer answer the prompt that follows. The cursor is restored when a selection menu is interrupted. - Color:
NO_COLOR,CLICOLOR,CLICOLOR_FORCEandTERM=dumbare honored. Colored table rows now align. - Wording: counted nouns read
1 package/2 packagesinstead ofpackage(s).Error:/Warning:prefixes are consistent, and warnings go to stderr.--silentis errors-only, but a failing run still prints why. Output that came out in random order is now sorted.--helppages no longer show developer notes. - Behavior fixes:
get --dry-runandvex --dry-runno longer write anything, andvex -O -writes to stdout.scan --jsonnever stops at an interactive menu. API errors show the server's message instead of a raw JSON body.
- Progress lines: a status line clears itself on finish. It is never
drawn off a TTY, under
-
Reversal leaves no
.socket/residue.rollback,remove,vendor --revert, the hosted unwind and the GC sweeps now prune what they empty: an emptied redirect or vendor ledger is deleted together with the empty.socket/vendor/<eco>/and.socket/vendor/directories (per-entry vendored reverts prune their ecosystem husk; aredirect-state.json.corruptquarantine keeps its directory), emptiedblobs/,diffs/andpackages/stores are removed, and.socket/itself goes with the lock when nothing is left — so a fully unwound hosted or vendored project has no.socket/at all. Deliberately kept: the zero-patch.socket/manifest.json({"patches": {}}+ itssetupblock —list/apply/vexexit codes depend on it) and thesetup-owned.socket/.gitignore,gem-plugin-stampandbundler-plugin/(rollback never undoes setup).setup --removenow also removes an emptied.socket/. -
scan --prunesays what it skipped and what it could not finish. ThegcJSON sub-object gainsfailedVendoredEntriesplus the additiveskipped: {code, message}(lock_held|lock_io) andwarnings: [{code, detail}](vendor_state_write_failed,manifest_write_failed,cleanup_failed) keys, with matchingGC: …human lines, so a pass that could not take the lock or could not rewrite a ledger no longer reads as a clean all-zero sweep — and a lock I/O fault or a failed rewrite is never mislabelled as lock contention. A legacy manifest record migrated into the vendor ledger is reported as thevendor_manifest_record_migrated/vendor_manifest_migration_failedrun warnings; a corrupt manifest no longer fails a vendored run (standalonevendorstill fails closed on it). -
Vendored
get/scanname the patch they replace. Adownloadedrecord for a purl the vendor ledger holds at another uuid carriesoldUuidand the human[fetch]line reads(replacing <uuid>);get --mode vendored --dry-runprints[dry-run] Would download and vendor N patches. No changes made.on both identifier paths; the[note]andPatch record saved tolines are gone with the manifest. -
Agent-mode
getleaves nothing behind when it records nothing..socket/and.socket/blobs/are created only when a record is persisted (all-skipped and all-failed runs leave no.socket/), a same-uuidget <uuid>re-run rewrites neither the manifest nor the blobs, and a blob/diff fetch that lands nothing creates no.socket/blobs/ordiffs/— theCannot create blobs/archives directoryall-failed envelope is gone; an uncreatable cache dir is a per-entryFailed to write blob/archive to disk. -
ownership_not_restoredis a warning, not silence. A fileapplypatched (orrollbackrestored) whose ownership could not be put back to the original uid/gid now surfaces as anownership_not_restoredrun warning (warnings[]plusWarning (ownership_not_restored): …on stderr) instead of riding a successful result unseen; the mode is still restored. -
removeon ledger-only state. A missing manifest beside a vendor or redirect ledger that holds nothing for the identifier answersnot_found(exit 1) instead ofmanifest_not_found; a second--skip-rollbackon the ledger-only leftover of an earlier--skip-rollbackis refused withvendor_state_retained(wasnot_found); every matching vendor-ledger entry — detached or not — is removable through the ledger with--preserve-stateand drift-keeps honored exactly as on the manifest path; manifest entries are removed in sorted order, and the(not installed)line prints only when something was not installed.rollbackprintsNo patches found in manifestonly for an unscoped run with no work in any leg. -
setup --excludepersists after the prompt, under the lock. The exclusion list is written after discovery and confirmation (also on the already-configured path when the flag is explicit) as a read-modify-write underapply.lock; a held or unopenable lock, or a manifest that cannot be read or written, is reported asnot persisting --exclude: …instead of being swallowed.setup --checkreads the vendor ledger even without a manifest and, on a corrupt one, warnsunreadable vendor stateand reports avendor_ledgererror entry (verdicterror, exit 1) — neverconfigured;vexrefuses the same unreadable ledger outright (vendor_ledger_corrupt, see Changed);listdegrades a corrupt vendor ledger to aWarning: unreadable vendor ledger …line (muted by--silent) rather than an error;patch_setuptelemetry fires only for a successful, non-dry-run setup. -
repair/vendorstate hygiene.repairresolves installed copies through qualified ledger keys (gem?platform=, pypi?artifact_id=, maven?classifier=no longer read as "not installed"), puts a crashed rebuild's<uuid>.pre-rebuildset-aside back when it is the only copy, and reports an absent or empty blobs dir asNo blobs to clean up.; every artifact sweep (repair,rollback,remove,scan --prune) keeps going past one unremovable file and reports the failures afterwards;vendor's dropped-record reconcile saves per purl and counts a failed save asvendor_state_write_failed;vendor_marker_write_failedis the one marker-failure warning for every backend (cargo/golang/pypi'smarker_write_failedretired), and a pypi vendor whose informational marker cannot be written now succeeds with that warning instead of sweeping the wheel; npm, yarn (classic and berry) and pnpm reverts honor the drift-keep on an unwired entry like bun and legacy pnpm already did; the hosted replay no longer credits a byte-identical hatch rewrite as an edited file; a corrupt redirect ledger met by a hosted scan is reported once, not twice. -
Manifest inputs are validated before they become paths.
applyrefuses anafterHashthat is not a 64-hex blob hash or a uuid that is not a plain path segment and reads blobs through a symlink-refusing opener (a poisoned manifest or a plantedblobs/symlink can no longer read out of tree);rollbackdeletes patch-added files in every pnpm store copy and heals a patched twin of an already-original primary. -
Human chrome. The global-mode
Using <X> at: <path>banner moves to stderr so piped stdout stays clean; the empty-crawl hint ofscanandgetreadsRun your package manager's install first.instead of a fixed npm/yarn/pnpm/pip/cargo/go/mvn/composer list;vendor --revertand the no-manifest no-ops ofvendorandapply(andapply --check) build no API client, so theSOCKET_API_TOKENadvisories no longer print on hooked manifest-less runs, andrepairprints its token notice once. -
Telemetry and self-update robustness. The telemetry client uses a 2 s connect timeout (a blackholed endpoint no longer stalls every command for the full request budget), and
--updatemaps only a contention errno toupdate_in_progress— other lock failures surface their real cause. -
A normal
scannever creates.socket/. Report-only,--dry-run, zero-discovery and no-op runs (hosted or otherwise) no longer scaffold the directory or a lock file; a GC pass checks for a manifest before it locks. -
apply --silenton an all-unmatched manifest prints its error line — errors are never muted by--silent; and the no-manifest early exits ofapplyandvendorname the missing.socket/manifest.jsoninstead of "No .socket folder found" (the folder may legitimately hold setup files or vendored state). -
Hosted redirect hygiene. Missing project files no longer skip silently:
redirect_composer_no_lockfile,redirect_gem_no_gemfile(neither manifest nor lock present) andredirect_maven_no_pom(nopom.xml, no Gradle build) warn once per run; a present-but-corruptpackages.lock.jsonwarnsredirect_nuget_lock_unparseablebefore any config mutation; aCargo.lockwith several same-name+version[[package]]blocks and nosourcedisambiguation warnsredirect_cargo_lock_pkg_ambiguousand skips transactionally; a registry override of the wrong kind now warns the arm's missing-override code for nuget/gem/golang (previously a silent skip); the ledger'sredirect_nuget_sourceedit recordsaction: "added"whennuget.configwas authored from scratch; hosted-revert lockfile restores are atomic and mode-preserving (includingbun.lockb), and a FIFO or symlink squatting on a lockfile is refused instead of wedging the revert. -
Vendor backend parity. Gem reverts follow every other backend's drift-keep rule (genuine drift keeps artifact + ledger entry; converged files are silent; a missing
Gemfile/Gemfile.lockwarnsvendor_lockfile_missingand still removes the artifact); composer, maven and nuget reverts keep the artifact + ledger entry (kept_artifact, thevendor_revert_keptskip) while the livecomposer.lock/pom.xml/nuget.configstill names the drift-skipped entry's uuid dir — previously the dir was deleted under a<repository>/<add>that still routed at it — and remove it once nothing references it; the golang service leg stages its download and, when a re-download of a wired present copy fails, keeps the copy and directive instead of tearing them down; poetry/pipenv/requirements refuse symlinked targets (pypi_{poetry,pipenv,requirements}_symlink_unsupported) and every pypi flavor refuses a project file that changed between plan and write (pypi_{poetry,pdm,pipenv,uv}_changed) instead of clobbering it;pyproject.tomledits made bysetuppreserve CRLF line endings; an unreadable (EACCES / squatting directory or FIFO) redirect ledger is reported as unreadable and left in place instead of being quarantined as "malformed"; a blob-cleanup pass keeps sweeping after one unremovable file and reports the first error afterwards; the ledgers skip byte-identical rewrites. -
Hosted composer redirects no longer fall back to the pristine git source. Composer 1 and 2.2 LTS silently install the upstream commit from
sourcewhen the hosted dist fails; the rewriter now drops the entry's adjacentsourceblock (one fragment edit, reverted byte-for-byte) and warnsredirect_composer_source_keptwhen a hand-ordered source cannot be dropped. -
Hosted gem locks keep bundler's source order. The patch-registry
GEMsection is inserted where bundler sorts it, soBUNDLE_FROZEN=true bundle installon bundler ≥ 4.0.19 no longer refuses the converged lock. -
v1
Cargo.lockfiles redirect and vendor correctly. Hosted and vendored rewrites now follow the[metadata]checksum table and rewrite dependents' full-id references, socargo --lockedaccepts the lock (and the revert stays byte-identical). -
Hosted cargo pins every declaration of the patched version. Each version of a multi-version crate is pinned only in the declarations whose requirement selects it (a requirement matching several locked versions is refused
redirect_cargo_toml_dep_unrewritable), and workspace-member and in-root path-dependency manifests are pinned beside the root, socargo --lockedaccepts the redirected lock. Member discovery never follows a symbolic link, so nothing outside the project is rewritten. -
Hosted cargo refuses crates a pin cannot reach. A crate another
Cargo.lockpackage also depends on (a crates.io or git crate, or a path package outside the project) now warnsredirect_cargo_transitive_dependentsand is skipped instead of being reported redirected while that package compiled the unpatched copy; a transitive-only crate'sredirect_cargo_toml_dep_not_founddetail now says so and points to--mode vendored. A crate declared only with requirements the patched version does not satisfy (cargo resolves those declarations to another version) is refusedredirect_cargo_toml_dep_unrewritable, the Socket backend's code for the same shape, instead ofredirect_cargo_toml_dep_not_found. A project with NOCargo.lockhas no resolved graph to ask, so a crate declared beside any other dependency — anything but a path dependency on a manifest the same run pins — or beside a workspace member this run did not read (a glob, a member outside the project or behind a symbolic link) is refusedredirect_cargo_lockless_dependents(commit a lockfile, or use--mode vendored); a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. -
CRLF cargo projects redirect in hosted mode. All-CRLF
Cargo.toml,Cargo.lockand cargo configs are rewritten with their endings kept (they were refused), andremove/ rollback still find the recorded edits after a checkout converts the line endings. -
Hosted cargo
removerestores every byte, in any order. An appended registry block leaves the user's config exactly as it was (trailing blank lines or a missing final newline included) and a created config is deleted with the last block; v1-lock and multi-version patches, ledgers written by older CLIs included, can be removed in any order; and a crate declared with the same line in two sections gets both pins reverted. -
yarn 4.0.x checksums keep the lock's own spelling. Vendored and hosted berry rewrites write bare-hex
cacheKey: 10c0checksums when the lock does, soyarn install --immutableno longer fails with YN0028. -
npm 12 dual-lock projects vendor both locks.
vendorrewires apackage-lock.jsonnpm 12 keeps beside a committed shrinkwrap (else warnsvendor_npm_sibling_lock_unwired), and hosted runs warnredirect_npm_allow_remote(npm 12'sallow-remote=nonerefuses redirected tarballs unless.npmrcsetsallow-remote=all— which hosted mode now writes itself, see Added) andredirect_npm_legacy_client(npm 6 ignores a v1 lock'sresolved). -
Bun refusal safety: hosted compatibility is checked before removing an existing vendored patch, including during dry-run. Vendored preflight exemptions require live local lock tuples; a ledger retained by
rollback --preserve-statecannot bypass a refusal or hide it in a preview. Symlinkedbun.lockbfiles are refused before patching so their links survive, andvendor --silentkeeps refusal diagnostics on stderr. -
Bun projects: every text-lock generation is accepted, vendored refusals fire before any write, and every mode change unwinds.
bun.locklockfileVersion0 — the opt-in text lock Bun 1.1.39–1.1.45 write with--save-text-lockfile— joins 1 and 2 in the shared version gate, so hosted mode redirects it (golden fixturenpm/bun/lock-v0), vendored mode wires it and the lockfile inventory discovers it; a newer version is now refused with "update socket-patch" instead of a re-lock that would reproduce it. Workspace locks are refused only where Bun cannot consume the rewrite: hosted mode refuses a version-0 lock holdingworkspace:packages (redirect_bun_workspace_unsupported; deletebun.lockand re-lock with Bun ≥ 1.2, which writes version 1 — accepted; a plain in-placebun installbumps the version only when a workspace depends on another workspace, otherwise Bun 1.2.0 keeps version 0 and 1.2.23+ fail to resolve) and vendored mode refuses any pre-version-2 workspace lock before writing (vendor_bun_workspace_unsupported— Bun 1.2–1.3 resolve a workspace member's local tarball path relative to the member; deletebun.lockand re-lock with Bun ≥ 1.4, since an in-placebun installkeeps the existing version, or — for a version-1 lock — use hosted mode; a version-0 lock is told to re-lock with Bun ≥ 1.2 first, since hosted refuses it too), while purls already vendored (by the ledger at the selected uuid, or with every matching lock tuple already pointing into.socket/vendor/, so a superseding patch uuid re-pins in place), re-runs andrepairon such a lock keep working; a corrupt.socket/vendor/state.jsonmet by that preflight is reported asvendor_state_unreadablerather than a Bun lock code.scan --mode vendored,get --mode vendored(search and uuid paths) now preflight the Bun lock BEFORE any download: a malformed binary, unreadable, unsupported-version or pre-version-2 workspace lock marks the npm patchesfailedwith the vendor refusal code and detail, fetches nothing and records no patch — thescan/get <purl>path writes nothing under.socket/and exitspartial_failure,get <uuid> --mode vendoredexits 1 withstatus: "error"and writes nothing — where previously the record landed in the manifest and the vendor step failed afterwards (and a detached run over an alias install misreportedpackage_not_installed). The refusals stay visible under--silent(code-tagged stderr line),--dry-runpreviews them as the additivewould_refuseaction (the humanscanandgetpreviews both print the[would-refuse]lines). Valid binary locks are inventoried and patched directly without a Bun runtime; malformed binary locks reportbun_lockb_invalid,redirect_bun_lockb_invalid, orvendor_bun_lockb_invalidat the corresponding entry point. Hosted → vendored takeover now works for bun —scan/get --mode vendoredandvendorover a hosted-redirectedbun.lockclaim and replay that purl's hosted edit instead of refusingredirect_revert_failed, andvendor --dry-runprobes the takeover instead of promising it — as dorollback <purl>/remove <purl>of one of several hosted bun records; on a lock the vendored backend refuses (a pre-version-2 workspace lock)vendorand its dry run report the refusal BEFORE the hosted revert, leaving the purl hosted-patched instead of un-hosting it and then refusing. Nativebun.lockbedits preserve the dependency graph and unrelated package metadata while updating binary pointers, tarball integrity, and the package metadata hash. The hosted text rewrite keeps CRLF on the rewrittenbun.lockline. Real-Bun coverage now runs in CI: the hermetic hosted and vendored suites on Linux, macOS and Windows (Bun 1.4.2, plus 1.1.45 and 1.2.23 lock-era legs), and the production native matrix — 16 releases from 0.8.1 to 1.4.2 in hosted and vendored mode — on pull requests andmain(rows carrycliRevisionandcliBuildShaprovenance), with the corrected digest boundary (Bun verifies URL/local tarball sha512 from 1.3.10, not 1.3.14). Bun 1.1.39–1.3.9 also re-save a hosted URL or vendored local-tarball tuple WITHOUT itssha512on any later lock re-save (bun add,bun installafter a manifest change); that digest-less 2-tuple is now recognised as the CLI's own wiring — repeat runs heal the digest,repairrebuilds through it, androllback, scopedrollback/remove,vendor --revertand both mode takeovers unwind it to the registry line — where previously every re-save on those releases leftredirect_bun_entry_not_foundbesideredirected: 1, apartial_failurerollback andvendor_lock_entry_not_found/vendor_lock_entry_driftedrefusals. Seedocs/testing/bun-compatibility.mdandscripts/backtest-bun.py. (#245) -
Rollback after a Pipenv relock no longer refuses forever.
pipenv lock(andupdate, andinstall <other>before 2024) regenerates a redirected or vendored entry to registry shape on every Pipenv major; that is now the desired end state — the hosted edit retires and the vendored record is dropped (vendor_lock_entry_relocked) — instead of a permanent drift refusal that held every pypi revert and kept the orphaned wheel dir. A foreignfile/pathreference is still drift. -
Same-run
--vexattests lock-only pypi redirects. The confirmed purl is unqualified while the ledger records the API's artifact-qualified purl; both sides now match on the qualifier-stripped purl, so a lock-only Pipenv (or uv) checkout no longer exits 1no_applicable_patchesafter redirecting its lock. -
The Pipenv installer probe runs only when a patch targets the lock, warns only when the lock was actually rewritten, resolves
pipenvon absolutePATHentries only (a relative entry would have executed apipenvplanted in the scanned repository), finds.bat/.cmdshims on Windows, and takes only the token afterversion(never a strayPython 3.12banner). -
Hosted Python redirects now warn when installed files still contain upstream or modified bytes and omit those packages from same-run VEX. The read-only probe covers Poetry virtualenvs, repeats on re-scans, and uses persisted patch records if fetching fresh records fails.
-
Agent mode finds Poetry's out-of-tree virtualenv. Poetry keeps a project's virtualenv under
{cache-dir}/virtualenvs/<name>-<hash>-py<X.Y>by default, so after a plainpoetry installthe crawler saw noVIRTUAL_ENV/.venv/venvand fell through to the global interpreter:scan --mode agentpatched nothing for the project's dependencies (or the wrong interpreter) while reporting success, and a barerollbackpruned the manifest while the venv stayed patched. The crawler now reproduces Poetry's own placement —virtualenvs.create/in-project/pathandcache-dirfromPOETRY_*, the project'spoetry.tomland the userconfig.toml, the platform default cache dir, and Poetry's env-name hash — without running Poetry, and scans every-py<X.Y>sibling.poetry run socket-patch …andVIRTUAL_ENVkeep working as before. -
scan --mode vendoredworks from a lock-only Poetry checkout. Thepoetry.lockinventory was discovery-only, so a fresh clone with nothing installed was skipped withvendor_fetch_unverifiableeven though the lock records the wheel's sha256 (uv's lock vendored fine in the same scenario). The inventory now carries the pure-Python wheel's sha256 fromfiles(lock 2.x) or[metadata.files](lock 1.0/1.1), and the pypi fetcher resolves a hash-only entry through PyPI's JSON API by that digest (verified again after download;SOCKET_PYPI_JSON_APIoverrides the endpoint). Poetry 0.12's bare[metadata.hashes]names no wheel and still needs an installed copy. -
removeno longer drops the manifest entry of a drift-kept vendored purl. When the vendored revert keeps the artifact (kept_artifact— the lockfile drifted), the manifest entry is now kept too (skipped/vendor_revert_kept), matching the core RevertOutcome contract; previously the entry was deleted, stranding a live ledger entry with no backing record. An all-kept run exits 1partialFailurewithsummary.removed: 0(nevernot_found— the identifier matched). -
Rebuilding a missing gem, maven or nuget vendored artifact now updates the ledger. When
vendor/scan --vendorfound a wired project whose committed artifact was missing or broken, it rebuilt the artifact but kept the old fingerprint in.socket/vendor/state.json(the gem file inventory, the maven/nugetsha256, and the nugetpackages.lock.jsonpin). If the rebuild came from the other source (the patch service instead of a local build, or the reverse), the new bytes no longer matched the ledger. VEX and verification then reported the artifact as tampered,repaircould fail, andvendor --revertleftpackages.lock.jsonpinned to the patchedcontentHash. A rebuild from the patch service was also reported asalready_vendoredinstead ofapplied. The rebuild now records the new fingerprint and keeps the entry's original wiring records, so revert still restores the pre-vendor files. Ifstate.jsonhas no entry for the package, or only an entry from another patch uuid, the rebuild still runs but the ledger is left as it is, because the run has no pre-vendor originals to record. -
A prebuilt maven
.jar, nuget.nupkg, pypi wheel or npm tarball from the patch service must now contain the patched files. Checking its integrity hash only showed that the download was intact, not that the archive carried the patch. The archive was still written as-is and every file was reported as already patched, so an unpatched archive could be committed and then rebuilt on every run. Each patched file inside the archive is now checked against the patch's expected hash before the archive is used. On a mismatch,autobuilds the archive locally and warnsvendor_prebuilt_layout_mismatch, and--vendor-source=servicerefuses withvendor_prebuilt_required(npm fails the package with the detail). -
A prebuilt artifact that fails its integrity check is always refused. Under the default
--vendor-source=auto, npm, pypi, golang, composer and gem (both the.gemand its stub gemspec) printed a warning and built the package locally when the downloaded bytes did not match the integrity the patch service reported. Bytes that fail verification may have been tampered with, so these ecosystems now refuse the package in every mode, as cargo, maven and nuget already did. The refusal code isvendor_prebuilt_integrity_mismatch(npm fails the package with the integrity detail). Under--vendor-source=service, golang, composer, gem and pypi now reportvendor_prebuilt_integrity_mismatchinstead ofvendor_prebuilt_required. -
servicevendor source without an API client is refused. This affectssocket-patch-corecallers that pass aVendorServiceConfigwithsource: Serviceand noclient(the CLI always configures a client). Every backend used to build the artifact locally in that case, even thoughservicepromises that only the patch service's artifact is used. They now refuse withvendor_prebuilt_requiredbefore doing any work, the same way--offlineis already refused. -
Rebuilding a missing cargo vendored copy now honours
--vendor-source. When a wired project's committed crate copy was missing or stale,vendoralways rebuilt it locally from the installed source. Under--vendor-source=serviceit did so even with--offlineor without an API client, and reported success. The rebuild now uses the patch service's prebuilt crate like a fresh vendor does, soservicemode refuses (vendor_service_offline_conflict/vendor_prebuilt_required) when the service cannot be used, andautostill builds locally when it has no prebuilt crate.
- Release publishing decomposed into per-registry workflows. The
crates.io, npm, PyPI, and RubyGems legs of the
Releaseworkflow now live in their own workflows (.github/workflows/publish-{cargo,npm,pypi,rubygems}.yml). A release is still one dispatch —release.ymldispatches each leg at the release tag (scripts/dispatch-publish.sh) and watches it to completion — but after a mid-release failure any single registry can now also be retried standalone (Actions → the registry's publish workflow → Run workflow with the release version) without rebuilding: each leg checks out thev<version>tag and, where binaries are needed, takes them from the GitHub release's assets verified againstSHA256SUMS(release-run dispatches additionally pin the sums file by digest, and same-version leg runs serialize through a concurrency group). Registry-side trusted publishers must be re-registered against the new workflow filenames (the legs always run as top-levelworkflow_dispatchruns, so every registry's filename matching sees the leg's own file) — see docs/releasing.md § One-time registry setup.
v4.0 is the three-modes release. What began as an agent-style tool that
patches installed packages in place now offers three deployment modes,
selected with scan --mode <agent|vendored|hosted> (each mode's detailed
entries follow below; per-ecosystem mechanics live in docs/ecosystems.md):
-
Agent mode (the default, and the original behavior):
applypatches installed packages in place on the current machine —node_modules/, site-packages, the cargo registry cache, and so on — tracked in the local.socket/manifest and re-applied after installs by the hookssetupconfigures. Requires socket-patch (and Socket API access, or pre-fetched blobs) on every machine that installs dependencies. -
Vendored mode (
vendor, orscan --mode vendored): ejects each patched package into a committed.socket/vendor/<ecosystem>/<patch-uuid>/<artifact>and rewires the ecosystem's lockfile so the project consumes the vendored copy. After committing, a fresh checkout builds with the patched dependency on machines with no socket-patch installed and no Socket API access — fully offline/airgap-friendly, and the strictest install flags (npm ci,--frozen-lockfile,--locked,--deploy, …) verify the vendored artifact like any other. A committed ledger records the verbatim original lockfile fragments, sovendor --revertrestores them byte-exactly. Covered in v4.0: the whole npm family (npm, yarn classic, yarn berry node-modules, pnpm, bun), pypi (uv, requirements, poetry, pdm, pipenv), cargo, go, composer, gem, maven, and nuget. -
Hosted mode (
scan --mode hosted, new in v4.0): rewrites lockfiles / registry configs so ONLY the patched dependencies resolve to Socket-hosted, integrity-pinned artifacts on patch.socket.dev — no artifact bytes land in the repo and no CI changes are needed. The package manager's own integrity checking pins the patched bytes (tamper fails the native install), and re-runs are idempotent. Covered in v4.0: the npm family (package-lock/shrinkwrap, pnpm — including Rush monorepos — yarn classic, yarn berry, bun), pypi (requirements, uv), cargo, composer, gem, nuget, maven (fail-closed version suffixing + Trusted Checksums), and golang for references carrying agoproxyregistry override (free tier; otherwise Go stays vendored — see the documented NO-GO analysis).
All three modes feed VEX attestation, with a provenance marker per mode:
plain (agent), (vendored), and (redirected) (hosted).
- The
unlocksubcommand. Folded intorepair, which now deletes the leftover<.socket>/apply.lockfile as its final housekeeping step (skipped under--dry-run, refused withlock_heldwhile another live socket-patch process holds the lock). Rationale: a leftover lock file from a crashed run never blocked acquisition in the first place — the OS releases a dead holder's advisory lock along with its file handle — sounlock's inspect path had no recovery scenario, and its--releasefile deletion is now automatic. Migration:unlock --release→repair; the probe-style "is anything holding the lock?" check → run the mutating command (optionally with--lock-timeout) and branch onerrorCode: lock_held.SOCKET_UNLOCK_RELEASEis gone with the subcommand, and thepatch_unlocked/patch_unlock_failedtelemetry events are retired. - The global
--break-lockflag andSOCKET_BREAK_LOCKenv var. It never stole a live holder's lock (deliberately, since that defeats mutual exclusion) and a stale file never contends, so all it did was emit alock_brokenaudit event for a reclaim that plain acquisition performs anyway. Thelock_brokenwarning event and rollback'swarnings[]lock_brokenentry are no longer emitted (warningsstays present, now always empty). Thelock_heldstderr hint now advises waiting /--lock-timeoutinstead of pointing at the removed commands.
--helpcommand order is now workflow-first:scan,apply,vex,vendor,setup, thenrollback,get,list,remove,repair.
-
get --mode <hosted|vendored|agent>— per-advisory hosted/vendored patching.get(fetch/apply a single patch by CVE/GHSA id, patch UUID, or package name) now honors the same mode selector asscan: hosted rewrites lockfiles for just the selected patches through scan's redirect engine verbatim; vendored routes through scan's vendor step with scan's save-only download posture. CVE/GHSA fan-outs are narrowed to versions actually installed (disk ∪ manifest, plus the lockfile inventory and vendor ledger in hosted/vendored modes); when narrowing leaves nothing, the new additivenot_installedstatus reports it at exit 0. Exempt from narrowing: UUID ids, exact-versioned purls,--save-only,--all-releases, and the package-name path. -
Hosted mode for Go (free tier).
scan --mode hostednow redirects golang dependencies when the reference carries agoproxyregistry override: a fork-stylereplace <mod> <ver> => patch.socket.dev/gopatch/<uuid> <ver>-socketpatch.<n>ingo.modplus the socket module's twoh1:lines ingo.sum(and the replaced original's lines pruned — the tidy-stable state). Day-2 machines need no configuration: go consults the checksum database only for modules absent fromgo.sum, so the committed pair is the whole redirect — validated end-to-end ine2e_golang_hosted_build.rs(fresh caches, bogusGOSUMDBtripwire,go mod tidybyte-level no-op, tampered-hashSECURITY ERROR). Fails closed (per-depredirect_golang_*warnings, no partial writes) on missing hashes, an out-of-namespace module path, a require-version mismatch, or a user-authored replace conflict; references without the override keep the historicalredirect_golang_unsupportedwarning (paid tier stays vendored — seedocs/design/golang-hosted.md). Wire schema gainsintegrity.goModH1andregistryOverride.identifiers.goModuleVersion(additive). Requires server-side publication of the grant-freegopatchartifact flavor — production publishes no golang hosted modules yet, so behavior is unchanged until it does. -
Version-bump automation + release-readiness gate.
scripts/bump-version.sh <X.Y.Z> --prperforms the whole bump chore — stamps every packaging site viaversion-sync.sh, rolls[Unreleased]into a dated## [X.Y.Z]CHANGELOG section, and opens therelease/vX.Y.ZPR (also dispatchable from the Actions tab as the Version Bump workflow). A newrelease-readinessCI job runsscripts/release-lint.shon every PR: version-coherence always (version-sync must be a no-op, so a hand-edited version in any one packaging site fails CI), plus the full gate — non-empty CHANGELOG section, no pre-existing tag — on PRs that bump the workspace version. TheReleaseworkflow'sversionjob now runs the same script, so the publish gate and the PR gate cannot drift. Playbook: docs/releasing.md. -
socket-patch --update— self-update. Downloads the release for the compiled target from GitHub Releases, verifies it against the publishedSHA256SUMSbefore extraction, sanity-execs the staged binary, and atomically swaps it in place (Windows uses the rename-dance viaself-replace; a setuid/setgid install is refused).--update 3.4.0(orSOCKET_PATCH_VERSION) pins a version, up or down; bare--updatenever downgrades;--forcereinstalls.--dry-runis a check-only probe (zero downloads,updateAvailablein the--jsondetails). Package-manager-managed installs (npm, pip, cargo, the gem launcher cache, Homebrew) are detected from the canonicalized executable path and refused with that manager's own upgrade command;--forceoverrides.--offlinerefuses up front and--forcecannot bypass it. Concurrent updates are single-flighted via an advisory lock; every failure path leaves the installed binary untouched. -
Passive update notice. Interactive runs mention a newer release at most once a day, on stderr only, after the command's own output: suppressed under
--json/--silent/--offline, in CI, when stderr is not a terminal, or withSOCKET_NO_UPDATE_CHECK=1(suppressed means zero network I/O). The background check can never alter a command's exit code, stdout, or add more than ~500 ms; state corruption degrades to "never checked". An explicit--updaterefreshes the notice's cache. -
shellcheck scripts/install.shin CI (and a fix for the SC2144 glob-with--emusl-loader probe it found). -
socket loginnow configures socket-patch. The JS Socket CLI's persisted config (<data dir>/socket/settings/config.json) is read — never written — as a fallback layer below env vars forapiToken,defaultOrg, andapiBaseUrl: precedence per key is CLI flag > env varsocket-cli config > built-in default. Four
SOCKET_CLI_*env names are accepted as silent peer aliases (SOCKET_CLI_API_TOKEN,SOCKET_CLI_ORG_SLUG,SOCKET_CLI_API_BASE_URL,SOCKET_CLI_NO_API_TOKEN); the canonicalSOCKET_*names win. Two new env-only toggles:SOCKET_NO_API_TOKENignores ambient tokens (env + config; an explicit--api-tokenstill authenticates) andSOCKET_NO_CONFIGdisables the config layer. A corrupt config file warns once on stderr and is ignored;--jsonstdout is unaffected. The telemetry endpoint now resolves the API base through the same chain as client construction, so a config-suppliedapiBaseUrlapplies to both. Design notes:docs/design/configuration.md. -
Hosted patch mode:
scan --mode hosted(a.k.a. the hidden--redirect). The third patch-application mode: instead of applying in place (agent) or committing artifacts (vendored),scanrewrites lockfiles / registry configs so ONLY the patched dependencies resolve to Socket-hosted, integrity-pinned packages on patch.socket.dev — no artifact bytes land in the repo and no CI changes are needed. Per ecosystem: npm rewritespackage-lock.json/npm-shrinkwrap.jsonresolved+integrity(v2 legacydependenciesmirror included),pnpm-lock.yamlinline resolutions, and yarn classicresolved/integrityblocks; pypi rewritesrequirements.txtpins toname @ <url> --hash=sha256:…(pip-compile continuation lines are refused rather than corrupted) anduv.lockwheel entries; cargo defines a per-patch sparse registry in.cargo/config.tomlplusCargo.tomlregistry =keys and Cargo.locksource/checksumsurgery; composer rewrites the lock entry'sdisturl/shasum; nuget adds anuget.configsource +packageSourceMappingand repinspackages.lock.jsoncontentHash; gem adds a per-depsourceblock + aCHECKSUMSpin (bundler ≥ 2.6). A dep counts as redirected only when its hosted URL (or per-dep registry index) actually landed in a project file; re-runs are idempotent (zero new edits over already-rewritten output). The Rust rewriters are held byte-identical to the depscan backend's TS twins (the GitHub-app hosted PR flow) by shared golden fixtures undertests/fixtures/redirect/. JSON output gains aredirectsub-object withmode: "hosted",redirected,rewrittenFiles,skipped,warnings. -
scan --mode <hosted|vendored|agent>: the documented mode selector. One value-enum flag replaces the boolean spellings (--redirect== hosted,--vendor== vendored,--apply/--sync== agent), which remain supported as aliases. Combining--modewith a boolean of a DIFFERENT mode is a usage error (exit 2); the same mode spelled both ways is accepted, and--detachednow requires vendored mode in either spelling. -
VEX support for hosted mode: the
(redirected)provenance marker + the redirect ledger.scan --mode hostedpersists its recorded file edits and the full patch records (file hashes + vulnerabilities) into.socket/vendor/redirect-state.json(merge-on-rewrite, append-only edits — the pre-redirect originals a future revert needs are never clobbered). Redirected patches carry the impact-statement marker "Patched via Socket patch<uuid>(redirected)", completing the provenance trio (plain = agent,(vendored),(redirected)). In-runscan --mode hosted --vexattests confirmed redirects from the ledger WITHOUT hash verification (the bytes are fetched at install time; the JSONvexsummary carriesverified: false), while a post-installsocket-patch vexreads the ledger back and hash-verifies the redirected patches against the installed tree. A confirmed redirect whose record fetch failed surfaces arecord_fetch_failedwarning (the patch is missing from VEX until a re-run). -
NuGet + Maven vendor backends (
vendor/scan --mode vendored). NuGet: the uuid dir is a committed folder feed holding a deterministically rebuilt.nupkg(embedded signature dropped; unsigned is accepted under NuGet's default validation), wired via anuget.configsource +packageSourceMappingand apackages.lock.jsoncontentHashrepin —dotnet restore --locked-modethen fails NU1403 on tamper. Maven: the uuid dir is a committed maven2file://repository (rebuilt.jar+ the verbatim upstream pom so transitives survive +.sha1sidecars), wired via apom.xml<repository>withchecksumPolicy=fail; multi-module aggregator poms (vendor_maven_multimodule_unsupported) and gradle-only projects (vendor_gradle_unsupported) are refused fail-closed, and the always-onvendor_maven_local_cache_shadowadvisory carries themvn dependency:purge-local-repositoryone-liner (a warm~/.m2copy silently shadows any repository). Both are proven by docker capstones against the real .NET SDK / Apache Maven (cold-cache,--network none, RED + TAMPER probes).nugetandmavenare now DEFAULT compile features; theSOCKET_EXPERIMENTAL_NUGET/SOCKET_EXPERIMENTAL_MAVENruntime opt-ins that briefly gated in-place agent apply were retired later in this cycle (see the "promoted to fully available" entry under Changed). The vendored path convention + uuid recovery rule now coversnugetandmavendirs, and--vendor-sourceprebuilt downloads cover nuget. -
Maven hosted rewriter (pom projects) — fail-closed version suffixing + Trusted Checksums. Hosted mode's maven leg pins the patched jar the only way a lockfile-less ecosystem can: the serve route exposes the patch under a Socket-only
<version>-socket.<hex8>suffix (existing ONLY on the injectedsocket-patch-<uuid>repository), and the rewriter pins that version explicitly — it rewrites the literal<version>, or (for a transitive / managed dependency with no literal version) adds a<dependencyManagement>entry — alongside the<repository>insert (releases enabled,checksumPolicy=fail, snapshots disabled). An outage or tamper on the Socket repo then HARD-FAILS the build: the suffixed version resolves nowhere else, so there is no silent fall-through to Central (the base version 404s). A${property}version is refused (redirect_maven_dep_unpinned— a literal edit would break the reference and a depMgmt pin could strand sibling artifacts); a literal version matching neither the base nor the suffixed value is skipped (redirect_maven_dep_version_mismatch); a non-jar<type>is skipped (redirect_maven_unsupported_packaging). When the serve route supplies both the jar and pom sha256, the rewriter also emits Maven 3.9+ Trusted Checksums files —.mvn/maven.configresolver args (originAware=false,failIfMissing=false) +.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 —redirect_maven_trusted_checksums_conflict). The.mvn/*files are silently inert below Maven 3.9 (the version suffixing is still fail-closed on its own); on 3.9.0–3.9.8 a mismatch is enforced but reported unclearly (readability fixed in 3.9.9, MNG-8182). When the upstream pom is unavailable / unsuffixable the rewriter falls back to the legacy same-GAV repository injection with aredirect_maven_same_gav_fallbackwarning (NOT fail-closed: a Socket-repo failure falls back to the unpatched artifact). Gradle build scripts are never edited: a presentbuild.gradle*/settings.gradle*emits a paste-ableexclusiveContentsnippet carrying the suffixed version (redirect_gradle_manual_snippet) plus a reminder to bump the dependency declaration — fail-closed by repository exclusivity. -
Hosted mode now rewrites yarn-berry and bun lockfiles. The hosted npm family gains two flavors beyond package-lock / pnpm / yarn-classic. yarn berry (
__metadata:v2+ lock): the rewriter edits ONLY the lock entry —resolution:gains yarn's own::__archiveUrl=<encodeURIComponent(url)>binding andchecksum:becomes the precomputedyarnBerry10c0cache-zip sha512 — leaving the descriptor key andpackage.jsonuntouched, soyarn install --immutable --check-cachepasses and tamper fails YN0018. Whole-file gates refuse acacheKey ≠ 10c0or a.yarnrc.yml compressionLevel ≠ 0(redirect_yarn_berry_cache_unsupported) — no offline-reproducible checksum. Validated e2e against realcorepack yarn@4.12.0on the node-modules linker; PnP is not exercised for hosted (the lock rewrite fires, but PnP's.yarn/cacheresolution is untested). bun (textbun.lockv1): the packages-entry registry 4-tuple["name@ver","<reg>",{deps},"sha512-…"]is rewritten to a URL 3-tuple["name@<url>",{deps},"sha512-…"], fail-closed on any grammar deviation;bun install --frozen-lockfilethen installs the hosted bytes and tamper fails the integrity check. A binarybun.lockbwith no text lock is auto-migrated first via the user's ownbun install --save-text-lockfile --frozen-lockfile --lockfile-only(deletesbun.lockb, recorded as aremovedledger edit, offline, fails closed;redirect_bun_lockb_would_migrateon--dry-run,redirect_bun_lockb_unsupportedif the migration is unavailable). The Rust rewriters are byte-identical to the depscan backend's TS twins via shared golden fixtures. -
Hosted mode supports Rush monorepos. A Rush repo has no root
package.json/lockfile pair — its pnpm source-of-truth lock lives atcommon/config/rush/pnpm-lock.yaml(plus one per subspace undercommon/config/subspaces/<name>/).scan --mode hosteddiscovers those locks whenrush.jsonis present and repoints them in place (the pnpm rewriter is now basename-generalized, so nested locks rewrite path-generically). Editing a Rush lock outsiderush updatedesyncs thepnpmShrinkwrapHashincommon/config/rush/repo-state.json, so aredirect_rush_repo_state_stalewarning fires when a lock was touched and that file exists —rush installfails underpreventManualShrinkwrapChangesuntilrush updaterefreshes it, but the redirect survives the refresh (pnpm keeps locked resolutions for unchanged specifiers). Agent mode already works through Rush's generated project symlink farm; vendored mode is refused (vendor_rush_unsupported) becauserush installcopies the lock intocommon/temp, so vendor's relativefile:specs can't survive — the refusal routes to hosted mode. -
pnpm hosted rewriter generalized to nested lockfiles. The
pnpm-lock.yamlrewriter now matches anypnpm-lock.yamlat the project root OR at any nested path (*/pnpm-lock.yaml), so Rush subspace locks and other nested-lock layouts are rewritten in place under their repo-relative keys. Write-back and confirmed-redirect gating are path-generic. -
Golang hosted mode is a documented NO-GO. Hosted redirect for Go is deliberately unsupported — sumdb hard-fails the patched pseudo-version on every day-2 machine and the only escapes are uncommittable machine-local config; Go's module-path identity would force per-grant artifacts against the build-once converter; and the default
GOPROXYchain would leak licensed bytes / tokened URLs to the public mirror. The full analysis lives indocs/design/golang-hosted-no-go.md; both the CLI rewriter and the depscan backend twin emitredirect_golang_unsupportednaming the remedy (use vendored mode, which gives Go everything hosted promises elsewhere). The one sanctioned exception — an ephemeral-CI GOPROXY recipe — is documentation-only and never written into a repository. -
vendornow supports every major npm and pypi package manager. The npm ecosystem gained four lockfile flavors beyondpackage-lock.json— yarn classic (yarn.lockv1), yarn berry with the node-modules linker (resolutions+ a cache-zip10c0checksum reproduced offline from the vendored tarball), pnpm (pnpm.overrides+pnpm-lock.yamlsurgery, pnpm 9 & 10), and bun (bun.lock) — all sharing the one vendored tarball and selected by a content-sniffing probe (yarn-berry PnP and bun's binarybun.lockbare refused with pointers to the native flow). The pypi ecosystem gained poetry, pdm, and pipenv (lock-only[[package]]/ entry splices, like the existing uv/requirements flavors). Every lockfile checksum/reference field for a vendored package is now recomputed coherently (the v2 "update checksums and references" directive); the gem backend handles bundler ≥ 2.6's optionalCHECKSUMSsection; composer'sdist.referencecarries the patch UUID intoinstalled.json. Each flavor has a real-package-manager build-proof capstone (fresh-checkout, cold-cache, strictest-install —--frozen/--immutable/--deploy/--locked— with byte-identical revert).vendor --force/--revertaccept empty env vars (SOCKET_FORCE=) as false, matching the global-flag contract. -
New
vendorsubcommand: committable vendoring of patched dependencies. Whereapplypatches installed packages in place (machine-local state),socket-patch vendorejects each patched package into a committed.socket/vendor/<ecosystem>/<patch-uuid>/<artifact>and rewires the ecosystem's lockfile so the project consumes the vendored copy — after committing, a fresh checkout builds with the patched dependency on machines with no socket-patch installed and no Socket API access. Per ecosystem (each mechanism validated against the real package manager): npm rewritespackage-lock.jsononly (deterministic patched tarball, recomputed integrity,npm ci-verified); cargo writes a[patch.crates-io]entry in.cargo/config.tomlplus surgical Cargo.lock edits socargo build --locked --offlineworks; golang reuses thereplace-directive engine pointed at the vendor tree; composer rewrites the lock entry to adist: pathcopy; gem edits the Gemfile + Gemfile.lock pair in bundler's canonical form; pypi rebuilds a valid wheel (regenerated RECORD) wired through uv'spyproject.toml/uv.lockpair (uv-first) or requirements.txt (pip/uv pip). The patch UUID is recoverable from the lockfile path string alone (a documented convention for external tools), a committed.socket/vendor/state.jsonledger records the verbatim original lockfile fragments, andvendor --revertrestores them byte-exactly.vendor --vexmirrorsapply --vex; VEX generation attests vendored patches by hashing the committed artifacts, andapplyyields ownership of vendored packages (vendoredskip reason). -
Cargo support (
cargois now a default feature).applypatches a Rust dependency in place wherever the crawler finds it — the projectvendor/directory or the shared$CARGO_HOMEregistry cache — rewriting the crate's.cargo-checksum.jsonsidecar socargo buildaccepts the modified files.rollbackrestores the original bytes from thebeforeHashblobs, like npm/PyPI/gem.cargoships on by default (alongside the always-on npm + PyPI- Ruby gems support), so released binaries and a plain
cargo install socket-patch-clipatch Rust dependencies out of the box;maven/composer/nuget/denoremain opt-in.
- Ruby gems support), so released binaries and a plain
-
Project-local Go
replace-redirect backend (golang, default feature). The Go module cache is shared, read-only and checksum-verified, so in-place patching would failgo.sumat build time. Insteadapplywrites a project-local patched copy under.socket/go-patches/<module>@<version>/and a managedreplacedirective in the projectgo.mod, so the patch is project-scoped and the cache stays pristine for sibling projects.rollbackcleanly drops thereplacedirective + copy.apply --checkis a read-only, lock-free, offline auditor that verifies the committed redirects match the manifest, exiting non-zero on drift (for CI / GitHub-App use). -
Inline OpenVEX generation on
applyandscanvia--vex <path>. A single successfulapply/scancan now both patch and emit the OpenVEX 0.2.0 attestation, instead of requiring a separatesocket-patch vexstep. The--vex-product/--vex-no-verify/--vex-doc-id/--vex-compactflags mirror the standalonevexknobs (and reuse theSOCKET_VEX_*env vars). The document is always written to the given path (never stdout, so it never races--json), built from the post-run manifest and verified against on-disk state. JSON output gains a top-levelvexsummary ({ path, statements, format }). A requested-but-failed VEX makes the command exit non-zero even when the apply/scan itself succeeded, surfacing a stable error code in the envelope.
-
install.shcan install without reaching github.com. NewSOCKET_PATCH_BASE_URLpoints the archive downloads at any releases base that answers GitHub's two asset paths — notablyhttps://install.socket.dev/patch/SocketDev/socket-patch/releases, which relays them from the GitHub release, so one URL template covers either origin. A new release needs no publish for this: the origin resolves "latest" per request.socket-patch --updatecan use the same host today through theSOCKET_UPDATE_BASE_URLoverride it already has. Also new:SOCKET_PATCH_INSTALL_DIRto choose the install directory explicitly instead of taking/usr/local/binor~/.local/bin. The default download origin is still GitHub — seedocs/installer-hosting.md. -
The documented one-liner installs from
https://install.socket.dev/patch. The previous URL wasraw.githubusercontent.com, which asks users to trust a third-party CDN for a script they pipe into a shell and is the first URL a locked-down egress policy blocks. The hosted copy is byte-for-bytescripts/install.sh, with its SHA-256 published atinstall.socket.dev/patch.sha256; the GitHub raw URL keeps working and serves the same bytes. Binaries are still downloaded from the GitHub release and verified against itsSHA256SUMS— the trust model is unchanged, only the script's origin moved. New:docs/installer-hosting.md(how the copy is published), a CI step that runs the installer end to end instead of only linting it, and aninstaller-driftworkflow that checks the hosted copy against this repository weekly. -
Maven and NuGet promoted to fully available — the
SOCKET_EXPERIMENTAL_MAVEN/SOCKET_EXPERIMENTAL_NUGETruntime gates are retired. Every flow (scanin all modes,apply,get,rollback,vendor,repair,vex,setup) now discovers and patches installed Maven and NuGet packages unconditionally; the "N patch(es) skipped — support is experimental" warnings are gone, and the previously#[ignore]d maven/nuget dispatch e2e tests now gate CI. Setting the old env vars is harmless but does nothing. Behavior notes: a defaultscannow walks the local Maven repository (~/.m2/MAVEN_REPO_LOCAL) and the NuGet caches, andscan --prune/--syncnow judges maven/nuget manifest entries like any other ecosystem's (previously they were exempt from pruning while the gate was closed). The in-place sidecar caveat is unchanged and now documented per mode indocs/ecosystems.md: agent-mode patching leaves Maven's.jar.sha1/.jar.md5stale and NuGet's fixup deletes.nupkg.metadata+ advises on.nupkg.sha512; the vendored/hosted modes never touch the caches. -
Release workflow consolidated into a single
release.yml. One dispatch now publishes every package — crates.io, npm, PyPI, and RubyGems (both gems), all via OIDC trusted publishing — with the launcher-gem job gated on the GitHub release existing. The separaterelease-ecosystems.ymlworkflow is removed (itsrelease: publishedtrigger never fired: the release is created withGITHUB_TOKEN, which suppresses downstream workflow events). The CLI is distributed via GitHub releases, npm, PyPI, crates.io, and RubyGems only — the Composer/Packagist, Maven Central, and NuGet launcher channels drafted earlier in this cycle were dropped before ever shipping in a release. -
--api-url/--proxy-urlno longer carry clap-level defaults: with neither flag nor env var set they parse as unset and the documented default URLs are applied at API-client construction (after the socket-cli config layer). Observable behavior is unchanged unless a socket-cli login exists. -
All ecosystem feature flags removed — every ecosystem is always compiled in. The
cargo,golang,maven,composer,nuget, anddenoCargo features are gone from both crates; npm, PyPI, Ruby gems, Go, Cargo, NuGet, Maven, Composer, and Deno support is now unconditional. Builds that passed--features <eco>will get an "unknown feature" error and should simply drop the flag;--no-default-featuresno longer produces a minimal binary (there is nothing left to strip). TheSOCKET_EXPERIMENTAL_MAVEN/SOCKET_EXPERIMENTAL_NUGETruntime gates outlived this entry only briefly — they are retired in the same release (see the "promoted to fully available" entry under Changed). The only remaining features are the test-suite gatesdocker-e2eandsetup-e2eonsocket-patch-cli. (MAJOR for anyone scripting--features; no behavior change for default builds beyond composer/deno support now being present.) -
Token-less
scannow batch-queries the public proxy. Proxy-mode scans POST{proxy}/patch/batch(one request per--batch-sizechunk, mirroring the authenticated/v0/orgs/{slug}/patches/batchendpoint) instead of issuing oneGET /patch/by-package/:purlper package. The client transparently degrades to the legacy per-package GET path against proxies that predate the batch endpoint, and when the all-or-nothing batch validation rejects a chunk (e.g. a crawled PURL type the server doesn't recognize, such aspkg:jsr/…— per-package queries tolerate those individually, so one exotic package can't fail a whole scan). Rate limits and over-capacity 503s still surface instead of silently degrading. (MINOR)
-
bun 1.4 lockfiles are accepted again. bun 1.4.0 bumped
bun.locklockfileVersionto 2 while leaving the emitted grammar unchanged; the shared version gate refused everything but 1, so hosted and vendored modes refused every lock written by bun ≥ 1.4 (redirect_bun_lock_unsupported/vendor_lockfile_version_unsupported). The gate now accepts versions 1 and 2 and keeps failing closed on anything else; new shared golden fixturenpm/bun/lock-v2(the depscan TS twin needs the matching acceptance + fixture sync). -
gem: every coexisting installed copy is patched. Bundler's scoped
<engine>/<abi>/gemsand flatgems/stores can coexist under oneBUNDLE_PATHroot, each holding a real copy of the same gem@version; first-wins resolution patched one store and reported success while the other bundler loaded pristine (vulnerable) bytes.applynow fans out per copy (per-copyAppliedevents, each counted insummary.applied), bundle-path roots are crawled in bundler precedence order, and config-sourced roots are contained. Thegem_bundle_config_path_ignoredwarning also prints the skipped path verbatim instead of backslash-escaped. -
npm
@socketsecurity/socket-patch: the./schemaexport is now built at publish. The subpath pointed at a gitignoreddist/directory that nothing built during release, so it shipped broken; aprepackscript now compiles it as part ofnpm publish. -
Release workflow tag-guard and idempotency fixes. The tag-already-exists guard never fired (it ran
git rev-parsein a shallow, tagless checkout) — it is now a statelessgit ls-remotecheck that still permits same-commit retries; the GitHub-release step re-runs cleanly instead of hard-failing when the release already exists; and the cargo/PyPI/gem publish jobs skip already-published versions, so "Re-run failed jobs" can resume a partial release safely. -
NuGet hosted rewriter: creating a
packageSourceMappingfrom scratch now emits a catch-all for pre-existing sources.packageSourceMappingis exclusive — once ANY mapping exists, every package must match some source's pattern or restore hard-fails NU1100. A redirect into anuget.configwith no prior mapping previously routed only the patched id, breaking every OTHER package's restore; the rewriter now fans a<package pattern="*" />mapping out to each pre-existing package source (longest-prefix match still routes the patched id to the Socket source). Golden fixtures updated on both the Rust and TS sides. -
VEX now attests Go
replace-redirect patches.socket-patch vexpreviously verified golang patches against the pristine module cache instead of the patched.socket/go-patches/copy, so redirect-applied patches were silently omitted from the document (reportednot_applied, orpackage_not_foundon cache-less CI). Verification now follows the managedreplacedirective to the committed copy. -
repairon a hosted-only project is an informational no-op. Hosted (--mode hosted) mode leaves no local artifacts to repair — the lockfiles point atpatch.socket.devURLs, and there is no manifest or vendor ledger. A project whose only.socket/trace isredirect-state.json(no manifest, no vendor ledger, no vendored lockfile references) previously errored withmanifest_not_found(exit 1); it now exits 0 with aredirect_only_projectskip pointing atscan --mode hosted. Repair still errors on a bare directory with no traces at all.
A repo-wide correctness, security, and filesystem-safety hardening pass: every
source file in both crates was reviewed line by line, the bugs found were fixed,
and regression tests were added throughout (the lib + integration suites grow by
~10k lines of mostly tests). The audit harness used to drive the review lives in
scripts/study-crates.ts.
- Path-traversal in archive extraction.
read_archive_to_map(patch/package.rs) validated the raw tar entry path but returned thepackage/-stripped path, so an entry likepackage//etc/passwdpassed every check and then resolved to an absolute/etc/passwdthatPath::joinwrites outside the package tree. Validation now runs on the normalized path actually written to disk. - Unbounded preallocation from an untrusted delta header.
apply_diff(patch/diff.rs) reserved aVecsized from the bsdiff target-size header, which qbsdiff never validates — a tiny hostile delta could claim up toi64::MAXand abort the process. The hint is now clamped to 64 MiB. - Evidence-free VEX attestation.
verify_patch_record(vex/verify.rs) returnedappliedfor a patch touching zero files, producing anot_affectedstatement with no on-disk evidence; zero-file records are now omitted (no_files).
applycould not write into read-only directories (Go module cache marks dirs0o555); added aDirWriteGuardthat temporarily grants write on the parent dir around the CoW-break + atomic rename and restores its exact mode.applystripped setuid/setgid bits on every patched file becausechownran afterchmod; reordered to chown-before-chmod, plus a parent-dirfsyncso the rename survives a crash.- Non-atomic symlink break (
patch/cow.rs) removed the file before staging its replacement, destroying it with no rollback on a failed write; now rename-over the link, matching the hardlink path. Stage files are cleaned up on every error arm. rollbackused an unsafe in-place write; it now delegates to the hardenedapply_file_patch(atomic, CoW-safe, validate-before-write, permission restore). Also: a GC'd before-blob no longer shadows the already-original short-circuit, and new-file deletion works inside read-only directories.- Hash integrity:
compute_file_git_sha256(patch/file_hash.rs) opened and stat'd the path separately (TOCTOU) and never checked the target was a regular file (a directory hashed as the empty blob); now opens once, fstats the descriptor, and rejects non-regular files.compute_git_sha256_from_readernow errors when the streamed byte count disagrees with the declared size. - Sidecar writes in read-only caches: the cargo
.cargo-checksum.jsonrewrite and the NuGet.nupkg.metadatadelete used bare, non-atomic I/O that failedEACCESin the locked-down registry trees they exist to serve; both now go through the hardened write/DirWriteGuardpaths. - Blob cleanup (
utils/cleanup_blobs.rs) aborted the whole sweep on one dangling symlink and inflated the "checked" count with subdirs/dotfiles; now usessymlink_metadata, skips stat errors, and counts only real blobs. - Lock acquisition (
patch/apply_lock.rs) mapped everyflockerror toHeld(maskingENOLCK/EACCES/unsupported-FS and busy-waiting through the whole timeout) and overshot sub-100 ms waits; genuine faults now surface immediately and the sleep is clamped to the remaining budget.
- Composer: normalize the
v-prefixedinstalled.jsonversion against bare PURLs, tolerate a single malformed entry instead of dropping the file, and skip packages absent on disk. - Go: only skip
cache/at the module-cache root (not at any depth), decode/encode case-escaped versions (v1.0.0-RC1↔…-!r!c1), treatGOPATHas a path list, and reject malformed/emptymoduledirectives. - npm: follow symlinked directories during the global-fallback walk
(
DirEntry::metadata()doesn't follow links) and guard nested recursion so it doesn't descend through symlinked packages. - NuGet: lowercase the version directory (not just the id) when resolving the global packages folder, so prerelease-cased versions resolve.
- Python: the macOS framework
Versions/layout uses bare3.11dirs, and a package with missing/malformedMETADATAnow falls back to its<name>-<version>.dist-infodirectory name instead of vanishing. - Deno: correct the macOS cache path (
~/Library/Caches/deno), honorXDG_CACHE_HOMEon Linux, and treat an emptyDENO_DIRas unset. - Maven: strip XML comments before tag matching and handle self-closing / inline skip-sections so a commented or oddly-formatted POM can't leak a plugin's coordinates as the project's.
- Cargo: tolerate
[package]headers with comments/whitespace and split<name>-<version>dirs at the dotted version (handles numeric pre-releases). - Shared:
utils/fs::entry_is_dirnow follows symlinks, fixing symlinked package-dir discovery across every dir-walking crawler at once.
- API client: honor a
--proxy-urloverride on binary downloads (was re-derived from env), and make org selection, patch titles, and the individual-query batch capability flag deterministic / order-independent; hash comparison is now case-insensitive. - Version reporting:
USER_AGENTand telemetrycontext.versionwere hardcoded to1.0/1.0.0; both now derive fromCARGO_PKG_VERSION. applyno longer emits a spuriousFailedenvelope event for a release-variant whose first file isNotFound.- UTF-8 safety:
get/scan/removetruncated display strings with raw byte slices that panic on multi-byte API text; all use char-safe truncation. - Exit codes:
setupnow exits non-zero (notalready_configured) when apackage.jsonfails to parse, andrepairexits non-zero and fires failure telemetry on a partial download failure (also gates the offline dry-run "would download" event and threads throughbytes_freed). rollbackno longer miscounts zero-file records as already-original or double-counts no-ops in dry-run;unlockreportsreleasedfrom a pre-acquiresnapshot so a probe-created lock file isn't reported as removed.vexresolves qualified PyPI/Gem/Maven PURLs via the rollback-aware resolver so those patches are no longer dropped aspackage_not_found.package.jsonhandling: no longer panics on a non-object root or non-objectscripts, de-dups overlapping workspace patterns, handles bare*/**/deep globs, strips inline YAML comments, and preserves top-level key order (enabledserde_json'spreserve_order).- Smaller fixes: deterministic
listoutput ordering, case-insensitivefuzzy_matchtie-break,json_envelopestatus-invariant enforcement +oldUuidfield,lock_clisub-second timeout message, blob-fetcher all-skipped formatting, VEXStatement.timestampmade optional per OpenVEX 0.2.0, and VEX git-remoteurlparsing.
- Hundreds of regression tests added across the patch engine, crawlers, API
client, manifest,
package.json, VEX, and CLI command layers; the stalerepair/python_crawlere2e expectations were updated to the corrected contracts. Full suite green (--features cargo). - Added the
scripts/study-crates.tsper-file audit harness (with an example prompt config) used to drive this review.
-
Telemetry coverage for read-side + housekeeping + attestation commands.
scan,get,list,setup,repair,unlock, and the newvexcommand each emit apatch_<action>(and matching*_failed) event through the existing send path, joining the apply/remove/rollback trio that already shipped. Thescanevent carries per-tier counts (free_patches/paid_patches/can_access_paid), the ecosystems filter, and afallback_to_proxyflag;getcarriesuuid/tier/ecosystem/download_mode/fallback_to_proxy. -
scan+getautomatically fall back to the public proxy on 401/403 from the authenticated endpoint. A stale or revoked token no longer blocks access to free patches — the CLI logs a warning to stderr, swaps to the proxy, retries once, and tags the resulting telemetry event withfallback_to_proxy: true. The classifier is deliberately narrow: 404, 5xx, network, and rate-limit errors do NOT trigger fallback so backend issues stay visible.apply/remove/rollback/vexkeep their fail-loud semantics. -
SOCKET_OFFLINE(airgap mode) now disables telemetry universally.is_telemetry_disabled()honors the sameSOCKET_OFFLINE=1|truesignal--offlineuses for network suppression, so apply (and every future command) no longer attempts a 5-second telemetry POST againsthttps://api.socket.devwhen the operator explicitly requested airgap.
- New
tests/telemetry_e2e.rsend-to-end behavioral coverage: apply/scan/get/list emit telemetry against a wiremock recorder;SOCKET_OFFLINE=1produces zero telemetry POSTs across all four; scan falls back on 401 + tags the resulting event; scan does NOT fall back on 500 (conservative classifier). - New
scan_invariantscases for the patch-management lifecycle: withdrawn patches keep their entry when the package is still installed but API is silent; entries for uninstalled packages get pruned;scanwithout--applyis read-only against the manifest and blobs even when an update is detected.
-
--offlinesemantics unified to strict airgap on every subcommand. Previously meant three different things acrossapply(strict airgap),repair(skip downloads / cleanup-only), androllback(fail when blobs missing). All three now mean the same thing: never contact the network, fail loudly when a required local source is missing. -
repair --download-modedefault changed fromfiletodiffto match every other subcommand. Users who need the legacy per-file blob behavior must now opt in with--download-mode file. -
repair --offlineis mutually exclusive with--download-only— passing both exits with code 2. -
Env vars renamed. The three remaining
SOCKET_PATCH_*env vars now use theSOCKET_*prefix:SOCKET_PATCH_PROXY_URL→SOCKET_PROXY_URLSOCKET_PATCH_DEBUG→SOCKET_DEBUGSOCKET_PATCH_TELEMETRY_DISABLED→SOCKET_TELEMETRY_DISABLED
The legacy names are still honored at runtime but emit a one-shot deprecation warning to stderr (the warning fires even under
--silentand--jsonbecause the transition signal must reach scripts and CI logs). Legacy names will be removed in v4.
- Shared
GlobalArgsclap struct#[command(flatten)]-ed into every subcommand. Every flag is now accepted on every subcommand (silently no-op'd where the subcommand doesn't consume it). Every flag has a matchingSOCKET_*env-var binding with precedenceCLI arg > env var > default. SeeCLI_CONTRACT.mdfor the full global-arguments table. applyandrepairaccept--api-url,--api-token,--orgvia the global flatten (previously env-var only — telemetry would silently fall back to the public proxy when the CLI was the only way to set these).- New global flags
--debugand--no-telemetry, promoted from env-only toggles. --proxy-url(env:SOCKET_PROXY_URL) as an explicit CLI knob for the public patch proxy.- New CI guard in the
Releaseworkflow: the workflow fails before tag creation ifCHANGELOG.mdlacks an entry for the version inCargo.toml. Blocks every downstream publish (cargo, npm, pypi).
- Garbage collection moved out of
apply. Usescan --prune,scan --sync, orrepair/gcinstead.applyis now strictly non-mutating against.socket/: when blobs need to be fetched they go to a temp overlay; the persistent cache is never written to. - Unified JSON envelope (
command/status/events/summary) forapply,list,remove,repair. Other subcommands keep their pre-v3 ad-hoc shapes for now; seeCLI_CONTRACT.mdfor migration status.
- Release workflow tolerates already-published npm packages so a partial publish can be retried without re-tagging.
- Pin Node
22.22.1in the release workflow to dodge a broken upstream npm.
- Harden core error handling, blob verification, and
--forcereporting. - Surface
find_by_purlserrors instead of silently swallowing them. - Add diagnostics to
applyfor silent no-op failures in CI. - Add explicit Node typings for TypeScript 6 compatibility in the npm wrapper.
- Simplify release to
workflow_dispatchonly (no bot commits). - Split release into PR-based version prep + auto-publish on dispatch.
- Prioritize
pnpm-workspace.yamldetection and restrictsetupto rootpackage.jsonfor pnpm monorepos. - Harden GitHub Actions workflows per
zizmoraudit. - Unflag Ruby gem (
gem) support and add e2e bundler tests. - Use
npx @socketsecurity/socket-patchfor the generated postinstall command.
- Full glibc/musl support across all Linux architectures (16 platform combinations now published per release).
- Interactive prompts and smart patch selection when multiple patches match a query.
- Ensure the binary has execute permission in the PyPI wrapper.
- Restore
binandoptionalDependenciesto the npm wrapperpackage.json.
- Expand ecosystem support: rough-in for composer, go, maven, nuget, ruby.
- Add a TypeScript schema library to the npm wrapper.
- Treat empty
SOCKET_API_TOKENas unset.
- Maintenance release.
- Maintenance release (version sync).
- Switch to per-platform
optionalDependenciesfor the npm package. - Add macOS global-package crawling fallbacks and pyenv support.
- Add support for more platforms; fix pypi and npm publish flows.
- Fix trusted publishing setup for npm and PyPI.
- Update PyPI publish action and add npm provenance permissions.
- Fix action image references in the publish workflow.
- Add
apply --force; rename--no-applyto--save-only(the old name remains as a hidden alias). - Cargo/Rust crate patching support behind a feature flag.
- Auto-resolve org slug from API token when
SOCKET_ORG_SLUGis unset.
- Fix publish workflow to checkout the bumped version.
- Pin GitHub Actions to full commit SHAs and wire up version-bump support in the publish workflow.