Skip to content

Latest commit

 

History

History
3554 lines (3430 loc) · 222 KB

CHANGELOG.md

File metadata and controls

3554 lines (3430 loc) · 222 KB

Changelog

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.

PRs never edit this file. Only the release agent writes [Unreleased], at release time, from the PRs merged since the last tag and the code they changed. Its ### headings set the next version's bump (Breaking/Removed → major, Added/Changed/Deprecated → minor, anything else → patch). Releases are cut by the release train (docs/release-train/DESIGN.md) with scripts/release.py: a release candidate's [Unreleased] entries become a ## [X.Y.Z-rc.N] section, the rolling release-sync PR brings each cut section and version back to main, and promoting an rc folds its rc sections into one ## [X.Y.Z] section. scripts/release-lint.sh refuses to release a version without a non-empty section in this file.

[Unreleased]

[5.1.0] — 2026-11-06

Added

  • RubyGems.
    • Agent mode patches gems with native (C) extensions for real. When apply (or scan / get --mode agent) patches a gem whose patch changes native-extension sources (such as ext/**/*.c or an extconf.rb), it now rebuilds the gem's extensions with RubyGems' own builder, the step gem pristine --extensions runs, using the Ruby that owns the gem home. Both the copy under extensions/<platform>/<abi>/ and the copy under the gem's lib/ are rebuilt, so Ruby stops loading the stale install-time .so / .bundle. A build that fails puts the original sources and build back (native_extension_build_failed), and a copy that can't be rebuilt in place (a precompiled platform gem, a Bundler git checkout, no matching Ruby) is refused with native_extensions_unsupported before anything is written. Sources an earlier release patched without a rebuild are rebuilt on the next apply (native_extension_rebuilt warning). vex and apply --check attest such a patch only once its rebuild is recorded, and otherwise report native_extension_not_rebuilt. rollback and remove restore the original build. Callers that compile the extension themselves can opt out with --no-gem-native-rebuild (env SOCKET_NO_GEM_NATIVE_REBUILD): the sources are written without a rebuild, with a native_extension_not_rebuilt warning, and vex does not attest them. Hosted copies are unaffected, since RubyGems compiles the patched gem itself (#1996, #1998).
    • Hosted mode replaces the gems the project already has installed. Bundler keeps a gem it already holds whatever source the lock names, so after scan / get --mode hosted repointed Gemfile.lock, bundle install printed "Bundle complete!" over the unpatched copy. The run now replaces those copies itself with the patched .gem the lock pins, checked against that sha256 first. It writes files earlier releases never touched: an installed gem whose gem home is inside the project (its gem dir, the home's cached .gem and, for a gem with a native extension, the extension dir that RubyGems rebuilds in a staging home), and a stale archive in Bundler's cache dir (vendor/cache, a committed file the run lists with the files to commit). rollback, remove and the withdrawn-patch restore do the same with the upstream gem, downloading it from rubygems.org when no copy is on disk. No bundle or gem command runs, --dry-run writes and downloads nothing, --offline uses only an archive already on disk, and the exit code never changes: a copy that can't be refreshed is left as it was. Gem homes outside the project (system gems, ~/.gem, an rbenv or asdf home) and bundle paths that resolve outside it are left alone unless --gem-refresh-shared (env SOCKET_GEM_REFRESH_SHARED) is given, and --no-gem-install-refresh (env SOCKET_NO_GEM_INSTALL_REFRESH) turns the refresh off. Each replaced copy is a redirect_gem_install_refreshed or upstream_gem_install_refreshed note (redirect_gem_install_would_refresh / upstream_gem_install_would_refresh on a dry run). What is still stale is one human warning with the remedy as a command (redirect_gem_stale_install, and the new upstream_gem_stale_install after an unwind), and the hosted Next steps: of a Bundler-only run say there is nothing to reinstall once the copies were replaced (#2052).
  • Go.
    • Run from the directory holding go.work, apply, vendor and hosted scans write one replace into go.work for a module the workspace shares, as go work edit -replace does (when that directory has no go.mod, another member lists the module, or go.work already holds Socket's replace). Before, a second member's replace was refused and a go.work root with no go.mod couldn't be patched at all. Hosted sum lines go to go.work.sum; --check, remove, vendor --revert, list and vex find the replace there; a stale committed workspace vendor/modules.txt names go work vendor; a replace of a module that is itself a use member is refused, and a symlinked go.work or go.mod is never rewritten (#1764).
  • General.
    • scan --mode hosted checks every patch uuid the lockfiles pin with the patch API, for packages the run covers. A pin the server reports withdrawn or not_found makes every fresh install fail with a 410, so the scan now restores it to its upstream registry entry, the same restore rollback runs. It reports a rolledBack event with patch_withdrawn, and --dry-run previews it. If a replacement patch is on offer, the same run pins it. A failed lookup keeps every pin and warns redirect_withdrawn_check_failed. A restore that can't run warns redirect_withdrawn_restore_failed (#1592).
    • scan names the advisories a run fixes and the ones it leaves unpatched. One patch is applied per package version while the results table lists the ids of every candidate patch, so every mode now prints, under its result line, <purl>: fixes <ids>; still unpatched: <ids> for each package it patched (would fix / would stay unpatched on a dry run), then one closing line saying that upgrading the package fixes the rest. scan --json carries the same on each packages[] entry the run selected a patch for, in the new selectedUuids, fixedIds and unfixedIds keys. An advisory is reported fixed only when the selected patch names it (#2052).
    • An agent-mode run that records a patch (scan --mode agent, agent-mode get) ends with a Next steps: block: commit .socket/, and run socket-patch apply after every install, locally and in CI, naming the install command of each patched ecosystem (bundle install, pnpm install, uv sync, …). After --save-only it says to run apply now, and a Go patch adds go.mod to the commit step and needs no re-apply. Nothing is printed for a dry run, a failed or global run, or under --silent / --json (#2052).
    • --ecosystems and socket.yml patches.ecosystems accept the names the output prints and everyday ones, in any case: python, ruby, go, php, pip, rubygems, rust, crates, java, gradle, sbt, dotnet, packagist, jsr, and node / pnpm / yarn / bun / vlt for npm. Before, -e go and -e python were rejected although the CLI prints "(5 go)", and socket.yml answered go with "did you mean gem?". Any other name is a usage error that lists the slugs (#2052).

Changed

  • General.
    • Packages the project patches itself, and local edits, are left alone unless you opt in. A Socket patch applied on top of a project's own patch leaves an install that fails or runs only one of the two, so this ships as a fix: socket-patch now detects the patches a project declares itself (pnpm patchedDependencies, Yarn patch: descriptors, patch-package files, npm and Bun patches, Composer patches, Cargo [patch] / [replace] and Go replace). scan (every mode) and apply skip such packages with a user_patch_conflict warning, and skips alone exit 0. Explicit get and vendor refuse them with exit 1. Agent apply also rejects installed files that match neither the patch's before-hash nor its after-hash instead of overwriting them; content an earlier Socket patch wrote (a stored .socket/blobs blob) is still overwritten with content_mismatch_overwritten. The new --allow-patch-conflicts flag is the explicit, per-run way to attempt the patch anyway (no environment or policy setting grants it) and emits user_patch_override. apply --force still overrides file drift but not project patches, and the remedy apply --check prints for drift on a project-patched package names the flag (#1646).
    • Hosted scans no longer download the full patch view for every confirmed redirect. In-run --vex fetches only the records it attests; the gem stale-install probe compares the gem's cached .gem with the patched artifact's sha256, and the Python probe checks the installed copy against the patched wheel the lock pins (downloaded only when a copy is installed). When neither can judge, the probe reads the patch view once; a copy still unjudged (fetch failure, --offline) is withheld by in-run VEX as stale_install_unverified. The Node binding still accepts a fetchPatch provider method but no longer calls it (#2021).
    • Plain vendor with a hosted pin in scope converts exactly the lockfile's hosted patches into .socket/vendor/ artifacts, without installed packages or upstream source downloads, and ignores .socket/manifest.json even when it exists (vendoring manifest patches takes scan --mode vendored). Dry runs report eject_planned, --offline is refused up front with offline_eject_unavailable, and the eject heals warm vlt store entries (redirect_vlt_reinstall_required) (#1648).
    • The npm package's @socketsecurity/socket-patch/schema export now depends on zod 4.6 (was 3.25) (#1702).
    • Bare rollback and remove <purl> now also delete orphan .socket/vendor/<eco>/<uuid>/ directories that no ledger entry owns, within the run's scope. One example is the losing side of a git merge of two vendor runs. Before, only vendor --revert and scan --prune swept them. An orphan that a lockfile still points into is kept and reported as vendor_orphan_still_wired, and --preserve-state keeps them all. A failed delete is now a cleanup_failed warning in rollback, remove, vendor --revert and scan --prune (#1610, #1683).
    • A bare scan covers every project in the git checkout. With no PATH, a hosted or vendored scan (the default mode included) now scans --cwd and every other project directory below it in the same git checkout, each as if it had been named: its own lockfiles, mode and .socket/, under a == <dir> == heading (== . == for --cwd), with one --max-new-patches budget and the worst exit code. A scan from a monorepo root therefore rewrites lockfiles in subdirectories that earlier releases left alone: they scanned the root only, or printed "No packages found." and exited 0 when the root had no lockfile. A project directory is one holding an npm-family, PyPI, Cargo, RubyGems, Go or Composer lockfile; Maven, Gradle, sbt and NuGet projects are still scanned only as --cwd or a named PATH. The search honours git's ignore rules (a .ignore file is not one of them), follows no link, never enters node_modules, .git, .socket, .yarn or vendor, never enters a git submodule or a nested clone (another repository: run socket-patch inside it), and leaves out a workspace member whose lock the workspace root's run reads. socket.yml path filters and the built-in test and fixture ignores apply, with one note counting what they left out. Nothing changes outside a git checkout, with a PATH, under --global / --global-prefix or in agent mode. --json and --vex still scan --cwd alone and name the directories they left in a new projects_not_scanned warning (#2052).
    • list and a bare rollback cover what such a scan patched: the project at --cwd and every patched project directory below it, under the same headings. list adds one total line and, under --json, a top-level projects[] beside the unchanged envelope for --cwd. rollback with no target now also rolls back the directories below, after one confirmation that names them, and exits with the worst code; --dry-run previews them all. It reaches exactly as far as a bare scan from the same directory: from an agent-mode project, whose scan covers that project alone, it rolls back that project alone and warns projects_not_rolled_back. rollback --json never changes a directory other than --cwd: it rolls back --cwd alone, as before, with the same warning, and with nothing to undo there it fails as it always did (manifest_not_found, exit 1). rollback <target>, remove, vex, repair and apply still act on one project and keep their exit codes. When --cwd holds nothing for them they add a note (patched_projects_below under --json) naming the directories below and the --cwd <dir> command for each; apply --silent does not search (#2052).
    • scan and get report "no patches" only when the patch service said so. An answer from another host (a mistyped --proxy-url or --api-url, a captive portal, a forward proxy), a 404 the service did not write, and the API's "Organization not found" for a mistyped --org are now errors; so is a lookup of the token-less per-package fallback that ends in a 5xx or in the proxy's "Patch API is not configured". Before, each read as "No patches available for installed packages." or "No patches found" with exit 0. scan goes on with the packages that were answered and reports the rest with a patch_lookup_failed warning and a failed event each: the run is partialFailure and exits 1 in every mode, and exits 1 with api_batch_failed when no package was answered. get exits 1 naming the host or the org slug to check. A lookup still throttled after its retries is reported the same way instead of failing its whole batch (#2052).
    • Manifest records no crawler can look for are named instead of dropped. A record of an ecosystem this CLI doesn't support (one a newer socket-patch wrote, or a mis-cased type such as pkg:NPM/…) is a skipped / unsupported_ecosystem event and one warning in apply, apply --check and rollback; it never changes the exit code, and apply --check no longer counts it as in sync. A key that is not a purl at all now fails: apply exits 1 naming it while the other patches still apply, apply --check reports drift (invalid_purl) and rollback a failed entry. remove refuses either kind before any change (exit 1) unless --skip-rollback is given. Before, apply printed "No patches to apply." and exited 0, and remove dropped the record without restoring anything (#2052).
    • repair verifies the blobs it finds. Every referenced blob is re-hashed, and one whose bytes changed on disk (a core.autocrlf checkout of a committed .socket/blobs/, a truncated file) is downloaded again and replaced. Before, repair said "All blobs are present locally" and every apply failed on the blob. repair --offline now exits 1 when a referenced blob is missing or corrupt, with a failed event per blob (offline_missing_sources / new blob_corrupt) and an error list --silent doesn't mute; it used to print "Repair complete." and exit 0. apply and rollback name a corrupt blob by its path and say to run socket-patch repair (#2052).
    • A failed checksum-sidecar fixup now fails the package. When apply or rollback can't bring cargo's .cargo-checksum.json, NuGet's .nupkg.metadata or the Maven and Coursier checksum files in line with the files it wrote, the files are put back, the event is failed ("sidecar fixup failed" / "sidecar resync failed") and the run exits 1: an apply leaves the package unpatched and a rollback leaves it patched. Before, the package counted as applied with exit 0 and only a sidecar_fixup_failed record under --json, which for cargo left a crate cargo build refuses. A run that finds every file already patched now rechecks and repairs those sidecars, and apply prints sidecar notes and warnings on stderr without --json (#2052).
    • The programs socket-patch runs (package managers and their version-manager shims, interpreters, git, a gem's extconf.rb and compiler) no longer inherit Socket credentials or settings. Their environment is stripped of the API token under every name a Socket tool reads it from (SOCKET_API_TOKEN, SOCKET_CLI_API_TOKEN, SOCKET_SECURITY_API_TOKEN and the _API_KEY spellings), SOCKET_CLI_CONFIG, SOCKET_CLI_API_PROXY, every flag-bound SOCKET_* variable and the Socket registry override URLs. Names are matched exactly, so HTTP_PROXY, HTTPS_PROXY, NO_PROXY, NPM_TOKEN and other registry credentials still reach the child (#2052).
    • vex --no-verify (also --vex-no-verify and SOCKET_VEX_NO_VERIFY) says what it did not check. Each statement emitted without on-disk verification ends its impact statement with ; not verified on disk, and the run warns vex_not_verified with the count. Before, an agent-mode patch whose files were unpatched or missing was attested not_affected with the same text as a verified one, with no warning. A verifying run's document is unchanged (#2052).
    • vex attests a vendored patch only while a lockfile reference wires its package to its artifact. A vendor ledger entry is no longer kept alive by text in the files the ledger names: a pnpm.overrides entry left in package.json after pnpm-lock.yaml went back to the registry, a Gemfile path: beside a reverted lock, or any file that merely spells the vendored directory now gets vendor_unwired and no statement. vendor --check applies the same rule; scan --prune still keeps such an entry (#2052).
    • A lockfile URL counts as a Socket patch reference only when every client reads its host alike and the patch server serves its path. A URL with a backslash, whitespace or a control character (https://patch.socket.dev\@evil.example/…), a percent-encoded / or \, or a path other than patch/<ecosystem>/…, patch-registry/<ecosystem>/… or gopatch/… (under any prefix on a --patch-server-url deployment) is no longer attested by vex or managed by list, rollback, remove and scan (#2052).
    • When two lockfiles wire one package to the same hosted patch and one of them lacks the integrity pin hosted mode always writes, vex no longer attests the patch from the lockfiles alone: the pinned lock doesn't vouch for the other (#2052).
    • vex no longer accepts a vendored reference whose artifact name carries a % (.socket/vendor/npm/<uuid>/%66oo-1.0.0.tgz): npm, yarn and pnpm open the literal file while pip percent-decodes it, so it names no single file to verify. No vendor backend writes such a name (#2052).
  • RubyGems.
    • Hosted mode keeps your gem declaration and always converges the lock. scan / get --mode hosted used to replace a gem's Gemfile declaration with a source "<patch registry>" do … end block holding the exact version, which lost the version requirement, quoting and comment for good, and it left a lock without CHECKSUMS alone, so frozen installs on Bundler older than 2.6 exited 16. It now appends one option, , source: "<index url>", to the declaration and changes nothing else. That covers parenthesized calls, and the multi-line, if-modified and ;-joined declarations that were refused before. A transitive gem gets one line at the end of the file (gem "<name>", "<version>", source: "<url>", require: false, marked # added by socket-patch (transitive dependency)). Gemfile.lock is always edited to the bytes the Bundler that wrote it produces, now also a lock with no CHECKSUMS section, which earlier releases did not write, so frozen and deployment installs accept the pair as committed. redirect_gem_no_checksums_section is raised once per run instead of once per gem, and a lock written by Bundler older than 2.2 warns the new redirect_gem_merged_lock_layout. A gem whose pair can't be made consistent is skipped with neither file changed (new redirect_gem_dependency_not_locked, redirect_gem_lock_unconvergeable and redirect_gem_inconsistent_plan), and a gem inside your own source / git / github / path block is now refused with redirect_gem_source_option where it used to get a nested Socket block. rollback and remove take the option or the added line off again, byte for byte. Block pins written by 5.0 / 5.1 stay recognized: a re-scan keeps the block and now converges its lock, and their unwind still restores the exact pin, with a new upstream_gem_declaration_pinned warning. The scan prints one line naming the declarations it changed (#2052).
    • vex attests a gem under a Socket section of Gemfile.lock only when the manifest beside the lock gives the gem that source: the source: option, the 5.0 / 5.1 source … do block, or path: to the vendored directory. With a lock that names the Socket source and a Gemfile that does not, Bundler fails a frozen install and an unfrozen one rewrites the lock and installs the upstream gem (checked on Bundler 1.17, 2.1, 2.4, 2.5 and 4.0), so that state is no longer attested: a hosted pin reads patched_ref_unattributable and a vendored one vendor_unwired. Both stay among the pins rollback and remove unwind. A declaration whose source: / path: is not a plain string literal, and a lock checked out without its manifest, are withheld too (#2052).
  • npm family.
    • A package the project patches itself (pnpm patchedDependencies, a yarn patch: descriptor or lock locator, patch-package files), including in workspace members, is never pinned by the in-memory hosted engine (skipped user_patch_conflict) and never attested by vex (vex_npm_patched_dependency), even when an --allow-patch-conflicts run wired it. A Bun patch made on top of the Socket pin no longer attests either, any pnpm-lock.<branch>.yaml withholds the pnpm lock pins whatever gitBranchLockfile says (vex_pnpm_git_branch_lockfile), and an unreadable lock or patch evidence outside the repository fails closed (#2018).
    • vex no longer trusts a lock the project tells its package manager not to read: npm package-lock=false (or shrinkwrap=false) in .npmrc, yarn 1 --install.no-lockfile in .yarnrc, a yarn berry lockfileFilename naming another file, or pnpm's lockfile setting turned off. A hosted pin in such a lock is attested only from an installed tree that verifies, and a vendored pin is omitted with the new vex_lock_not_read, except a pnpm pin that the override map pnpm reads still routes to the artifact. list, rollback, remove and vendor --check still see the pin (#2052).
    • vex withholds a pnpm pin (patched_ref_unattributable) when the lock installs a second copy of the same name@version: an entry from a git remote or a URL tarball, a file: copy whose package.json can't be read, or, in lockfileVersion 5 / 6 locks, another registry key of the release. A root-relative .socket/vendor/npm/… path in a per-member or Rush lock is reported as patched_ref_invalid instead of attested, since pnpm resolves it against that lock's own directory (#2052).
    • Run from a pnpm or yarn 1 workspace member, vex reads the member's own stray pnpm-lock.yaml or classic yarn.lock as absent, because the package manager installs the member from the workspace root: its pins are not attested, and one patched_ref_unattributable names the root to run from. Per-member pnpm locks (sharedWorkspaceLockfile: false) and a berry lock in a member are read as before (#2052).
    • Hosted scan and get run from such a member refuse instead of pinning a lock nothing installs from. A pnpm-lock.yaml inside a member that pnpm-workspace.yaml lists is never read by pnpm, which installs the member from the workspace's shared lock (checked with pnpm 10.34 and 11.27), and yarn 1 never reads a yarn.lock inside a package.json workspace member. Earlier releases wrote the pin there and reported the package switched while installs kept the unpatched copy. The run now exits 1 with redirect_workspace_lockfile_elsewhere, names the workspace root to run from and writes nothing. A workspace that sets sharedWorkspaceLockfile: false, a directory the workspace does not list and a yarn berry lock in a member are pinned as before (#2052).
  • PyPI.
    • Vendored uv refuses a patch for another version than uv.lock resolves (pypi_uv_lock_package_missing, exit 1), before any download and in the --dry-run preview. Earlier releases wired it with exit 0 by rewriting the lock entry's version, so a patch left behind by a dependency bump moved the lock down to the older release and uv sync installed it. With patches for two releases, the one for the locked release is wired and the other is refused. A project an earlier release vendored that way keeps working and still reverts (#2052).
    • vex withholds a Python pin that another entry or requirement installs unpatched. That covers a second entry of the same release in the same uv.lock, pylock.toml, poetry.lock, pdm.lock or Pipfile.lock that resolves from the index (a marker fork, an extras variant, the other Pipfile category), and a PEP 723 script lock that disagrees with the project lock: script locks now take part in the cross-lock check. Re-running scan or vendor rewires every entry and clears both. A reference behind an environment marker, extra, dependency group or Hatch environment is also withheld (patched_ref_unattributable) while a plain requirement can install the same release where the reference doesn't apply (#2052).
  • Cargo.
    • vex attests a Socket-sourced Cargo.lock entry only while a declaration cargo honors routes the crate to it: a live dependency pinned to the patch registry whose requirement can select that version, or a live [patch] registry pin in the root manifest or .cargo/config.toml. A lock entry nothing routes (a transitive-only crate, a shadowed [patch.crates-io] entry) makes cargo fetch --locked fail and an unlocked build link the upstream crate; vex attested it and now withholds it with patched_ref_unattributable. The entry stays among the pins rollback and remove unwind. Run from a workspace member, vex attests nothing from the member's own [patch] or lock, which cargo ignores for the workspace root's, and names the workspace root (#2052).
  • JVM.
    • vex withholds a Gradle statement when the derived-cache check of its patched copy doesn't finish: the purl is omitted as gradle_unpatched_copy with the new vex_gradle_derived_cache_check_failed warning. A check that crashed used to count as nothing to check (#2052).
    • Maven: a patch pinned only through <dependencyManagement>, for a package the root pom does not declare, is weighed against the parent poms. When a parent in the checkout, a grandparent or an active parent profile declares the package with a literal version, Maven builds the upstream jar, so vex omits the patch (vex_maven_parent_declares). When a parent cannot be read (<relativePath/>, a parent outside the checkout, a missing one), the pin is no longer attested from the pom alone: it reads package_not_found until a build has resolved the patched version. That withholds the pre-build statement in projects that inherit from a published parent such as spring-boot-starter-parent; the statement returns after a build (#2052).
  • Composer.
    • vex no longer attests a hosted Composer pin from the lock alone when composer.json puts the install outside the project (config.vendor-dir) and an install exists there that the crawler does not read: the patch is omitted with composer_install_unread. Set COMPOSER_VENDOR_DIR to have that tree read and verified (#2052).

Fixed

  • RubyGems.
    • With Bundler 4.1 override "other", with: "gem", in: "parent", hosted rollback and remove treat the override target as transitive and give back the original lock and Gemfile byte for byte, instead of writing a new exact gem "gem", "x.y.z" pin that blocked later upgrades (#1989, #1991).
    • Gemspec directives and development dependencies are read as Ruby code: =begin/=end blocks, heredoc bodies and __END__ data are skipped, so a commented-out dev dependency no longer adds a bogus requirement and a heredoc no longer refuses the Gemfile (#1932, #1936).
    • A gem declared inside nested source … do blocks is attributed to the innermost block, as Bundler reads it, so hosted rollback and remove of a pin inside the user's own source block no longer refuse with hosted_wiring_contested on Bundler 2.1 and older (#1997, #1999).
    • CRLF Gemfiles and Gemfile.locks: hosted and vendored writers keep the file's line endings instead of adding LF lines, revert restores both byte for byte, and vendored mode no longer fails a CRLF lock with "no GEM section"; a lock mixing endings fails closed (#1808, #1811, #1821, #1822, #1831, #1841).
    • A merged Bundler 1.17-2.1 lock with several hosted gems in one GEM section is read per Socket remote, so vex attests them and a re-scan no longer skips them as redirect_unattributable (#1749, #1750).
    • On Bundler 2.7+, a gemspec dev dependency merged into a later gem line is pinned with the requirement Bundler writes, so frozen installs stop failing; hosted warns redirect_gem_gemspec_dependency_unreadable when it can't read the gemspec, and vendored mode refuses the shape with gemfile_declaration_not_editable (#985, #1718).
    • vendor --dry-run over a hosted gem inside a group ... do block previews the refusal the real run gives instead of planning a clean eject (#1761, #1762).
    • A Bundler config setting disable_shared_gems to a false value is read like path.system, so apply patches the system gem homes Bundler loads, not a leftover vendor/bundle (#1931, #1943).
    • On a Bundler 1.17–2.1 lock, which has one merged GEM section, a superseding hosted patch now moves the lock's Socket remote along with the Gemfile source block. Before, every scan skipped the new patch as redirect_unattributable (#1688, #1690).
    • A superseding re-scan on a lock without CHECKSUMS can move only the Gemfile block. Hosted rollback and remove after it now also undo the newer patch's Socket source block, so the next bundle install no longer brings the patched gem back. Restoring only the newer pin is refused, naming the older patch (#1626, #1664).
    • Vendored unwinds (remove, rollback, vendor --revert, scan --prune) warn upstream_gem_stale_cache when bundle cache left a patched archive in vendor/cache, and name it, as hosted unwinds already did (#1594, #1667).
    • rollback and remove keep a gem's ! source pin in DEPENDENCIES when its source block opens after a ; or another expression, spans several lines, or uses { }. Bundler 4.0.19+ frozen installs no longer fail (#1509, #1520, #1531, #1537).
    • vex reads a gem library's product from its gemspec the way Ruby does, skipping comments, =begin blocks, heredocs and __END__. A commented-out spec.version or spec.name no longer sets the product purl (#1573, #1582).
    • Agent apply runs no Ruby for a gem's native-extension rebuild until the patch's files verify against the installed gem, and none on a dry run. Before, apply --dry-run evaluated the installed gemspec, a Ruby program that a committed vendor/bundle lets the scanned repository supply. The gemspec is now read as text, a dry run reports the pending rebuild as a native_extension_rebuilt warning, and refusal codes are unchanged (#2052).
    • Interrupting apply during a gem's native rebuild (Ctrl-C, SIGTERM) ends the build with it, instead of leaving ruby, extconf.rb and the compiler running into a gem no lock protects. The next apply, rollback or remove puts the previous build back first, then rebuilds or restores; before, it deleted the parked build or refused with "installed by a RubyGems older than 2.2". A gem that another process is still rebuilding is refused with native_extensions_unsupported (#2052).
  • npm family.
    • pnpm workspace globs with brace sets and character classes (packages/{a,b}, packages/[ab]*) are read the way pnpm reads them in every mode instead of being refused or read as every directory; shapes pnpm reads its own way (extglobs, [!a], a + after a class, stepped or punctuation-spanning brace ranges) are still refused, and commented importer lines in pnpm-lock.yaml are ignored (#1846, #1863).
    • Quoted values, YAML escapes and duplicate keys in pnpm-workspace.yaml, .yarnrc.yml and .modules.yaml decode one way, as pnpm reads them, so settings such as modulesDir or lockfileDir no longer resolve to different paths depending on the command (#1918, #1973).
    • yarn classic's .yarnrc is read as yarn 1.22 reads the whole file: --modules-folder 'deps' no longer sends the crawler to a folder yarn never installs into, and a file yarn can't parse sets nothing (#1901, #1915).
    • A malformed package.json workspaces field (a non-string item, null, the wrong type) is judged the same way everywhere; the Bun member walk falls back to bun.lock instead of reading no members (#1900, #1971).
    • apply and rollback no longer touch package copies inside a store nested in another store's entry, which scan never reports, and keep the peer-variant copies of a project that lives under ~/.bun, ~/.deno or ~/.vlt (#1899, #1986).
    • Hosted npm warns redirect_npm_omit_lockfile_registry_resolved when any npm config layer sets omit-lockfile-registry-resolved, which makes the next npm install drop the hosted URL and fail with EINTEGRITY, and names where it is set and the remedies (#1871, #1872).
    • Hosted vlt scans no longer download every granted patch archive to check its encoding, and no longer withhold dependencies with redirect_vlt_artifact_unverifiable; a mirror that re-encodes archives makes vlt ci fail closed with EINTEGRITY. The in-memory hosted engine (the Node addon and hosted-bundle) now pins vlt dependencies instead of withholding them as offline (#2013).
    • The lock inventory skips Rush subspace locks with unsafe names, as lockfile discovery already did (#2003).
    • vex no longer attests npm-family pins the hosted and vendored writers refuse: pnpm pins while gitBranchLockfile is on and a branch lock exists (vex_pnpm_git_branch_lockfile), and npm or Bun packages the project patches itself (vex_npm_patched_dependency) (#2010).
    • A registry dependency that the root package.json overrides send to a tarball URL, git or path spec (npm installs that spec, not the lock's resolved) is skipped by hosted mode with redirect_npm_non_registry_entry_skipped, refused by vendored mode, and no longer attested by vex, including overrides scoped to a workspace member (#1965, #1966).
    • pnpm locks are read the same way by every reader: pnpm 11's env lockfile document (config dependencies) is its own install tree, so its entries no longer contest the project's, and a config-dependency pin isn't attested while the project document resolves that name@version elsewhere (hosted names the copy with redirect_copy_unpatched). Rush subspace locks are always read beside the root lock. Vendored pnpm now edits a CRLF pnpm-lock.yaml or pnpm-workspace.yaml and writes it back CRLF (only mixed endings are refused, vendor_lockfile_crlf_unsupported); a shrinkwrap.yaml that a pnpm-lock.yaml shadows is left alone by hosted mode, and a pnpm 1/2 project beside one is refused by vendored mode (vendor_lockfile_version_unsupported) (#2009).
    • A commented-out importer key in pnpm-lock.yaml is no longer read as a workspace member, and every reader agrees on which importers a root lock lists (#1949, #1952, #1960).
    • Hosted yarn classic scans read a file: copy outside the project root (file:../ext/x) the way vendored mode and vex do, and name it with redirect_copy_unpatched instead of reporting a clean pin (#1951, #1984).
    • yarn 1 bundled copies (bundleDependencies unpacked under a parent) are no longer attested as patched: vex withholds them, hosted scans name them (redirect_copy_unpatched), and vendored scans warn vendor_bundled_instance_skipped (#758, #1712).
    • After a plain npm install re-saved a vendored lockfileVersion 2 lock without resolved in its legacy dependencies mirror, rollback, remove and vendor --revert restore both lock halves and remove the artifact instead of half-reverting and exiting 1 (#1937, #1938).
    • npm bins with a CRLF shebang: apply, apply --check, rollback and vex accept and write the LF form npm's bin-links give a linked bin, so a pristine install no longer fails as drift and a patched bin keeps running. Bins v5.0.1 left as #!/usr/bin/env node\r (exit 127) are repaired on the next apply; bins npm didn't link keep their CRLF bytes (#1756, #1757, #1791, #1792, #1804, #1805).
    • Run without --cwd, scan and apply look above the working directory as --cwd <abs> does: a parent .yarnrc --modules-folder, a root's relocated yarn pnpm store, and a root pnpm-workspace.yaml modulesDir are found from a member (#1902, #1906).
    • Agent mode crawls npm workspace members declared under a hidden directory the pattern spells (.github/actions/*, .config/tool) instead of leaving their node_modules unpatched (#1827, #1829).
    • Agent mode finds Yarn 4 pnpm-linker stores moved by pnpmStoreFolder (.yarnrc.yml, home rc or YARN_PNPM_STORE_FOLDER) inside the project, instead of reporting transitive packages not installed (#859, #1721).
    • Global scans find packages installed with yarn 1.0.0-1.0.2 yarn global add (no yarn global dir there), including a configured --global-folder (#437, #1798).
    • Deno global installs: scan -g, apply -g and rollback -g patch the $DENO_DIR/npm/<registry> copies deno install -g tools load (peer-variant copies included), --global-prefix $DENO_DIR or $DENO_DIR/npm expands to every registry below it, a registry served one path segment below another on the same host is no longer skipped, and a prefix naming a tool node_modules Deno never loads is a usage error (exit 2) (#444, #1719, #1741, #1784, #1785).
    • In a Deno project that also keeps an npm-family lock, hosted and vendored runs warn when deno.lock locks the same name@version (redirect_npm_deno_lock, vendor_npm_deno_lock), since deno install never reads the rewired lock (#406, #1711).
    • Hosted rollback and remove in a pnpm workspace with per-member locks weigh every member lock when deciding whether trustLockfile is still needed, and warn pnpm_trust_lockfile_left after restoring a member lock (#1268, #1781).
    • Vendored pnpm names each file: copy of the vendored release that the override can't reach (vendor_copy_unpatched) instead of reporting a clean success (#935, #1736).
    • yarn berry: hosted rollback / remove and vendored revert refuse, writing nothing, when the user has yarn patched the pinned package, instead of leaving a lock yarn install --immutable rejects (#962, #1733); vendoring beside a parent-scoped or glob resolutions entry for the package refuses with vendor_override_conflict (#783, #1705).
    • Vendored yarn classic reverts a lock block yarn re-keyed (merged or dropped a range) under its current key, so rollback, remove and vendor --revert no longer fail forever (#692, #1728).
    • A bun patch made after socket-patch rewired a package is keyed by the package's current resolution, the hosted URL or vendored tarball that Bun 1.4 writes. A superseding hosted run, a takeover or a vendored re-run moved the entry and orphaned that key, so the project's patch silently stopped applying. Scoped packages keyed this way were also unprotected. Such keys now protect the package. rollback, remove and vendor --revert warn redirect_bun_patched_dependency_dropped / vendor_bun_patched_dependency_dropped only when the unwind takes out a key the project's patch uses (#1019, #1699).
    • Bun workspace membership is read the way Bun reads it. Wildcards no longer match dot directories, so a project in packages/.tools with its own bun.lock is independent. ! exclusions are honoured for Bun 1.3.0/1.3.1 locks, which carry no config-version tag, and for a binary bun.lockb written by Bun 1.3+. Hosted and vendored runs from such a project now patch its own lock instead of refusing with redirect_workspace_lockfile_elsewhere / vendor_lockfile_missing (#1492, #1496, #1521, #1523, #1631, #1655).
    • Vendored npm scan --vex, vex and vendor --check no longer attest a package that a file: directory dependency also installs at the patched version (patched_ref_unattributable). When a git, folder, URL, file: tarball or bundled copy sits beside the vendored copy, vendor --check in npm and Bun projects now reports "copy unpatched" and names the lock entry. Before, it said the wiring was lost and gave re-vendor advice that looped (#1511, #1513, #1600, #1675).
    • Yarn 4's injectEnvironmentFiles (.env.yarn by default) are applied when ${NAME} is expanded in .yarnrc.yml. A nodeLinker set through .env.yarn no longer makes vendored mode refuse a node-modules project as Plug'n'Play, or agent mode skip a live PnP project as not installed (#1611, #1676).
    • A hosted scan of a yarn 1 workspace now refuses with redirect_yarn_classic_offline_mirror when a member's .yarnrc or .npmrc sets yarn-offline-mirror. Before, it pinned the root lock and reported success, installs from that member failed integrity checks, and vex attested not_affected (#1115, #1694).
    • A yarn.lock can also install the patched name@version under another dependency name: a renamed file: tarball in yarn classic, or any renamed file:, URL, portal: or link: copy in yarn berry. Hosted scans now warn redirect_copy_unpatched and vendored scans warn the new vendor_copy_unpatched. When the package is installed only under other names, vendored scans refuse with vendor_lock_entry_not_rewritable. Lock-only vex no longer attests the package (#939, #1236, #1489).
    • Yarn copies whose package can't be identified now fail closed instead of being dropped. This covers absolute, missing or empty portal: / link: targets; a manifest that doesn't parse or has a missing or invalid name or version; berry exec: dependencies; and git remotes that aren't http(s). Scans name these copies (redirect_copy_unpatched / vendor_copy_unpatched), and vex withholds the versions they may install. Copies outside the project, and tarballs whose top directory isn't package/, are read the way yarn reads them (#1516, #1517, #1540, #1551, #1561, #1571, #1579, #1586, #1598, #1605, #1630, #1644).
    • Yarn classic pins are compared with the patched package.json the way yarn 1 normalises it. A "" range, or a name listed under several dependency kinds, no longer drops the pin from lock-only vex. A dependency that every pinned block already names no longer needs a lock key of its own. That check refused patches with a remedy that couldn't clear it (#1558, #1578, #1596).
    • vex and scan --vex check the dependencies a patched npm package declares. The installed check finds the copy node actually loads and omits the patch with dependency_not_installed when that copy's version is outside the declared range. Lock-only vex of a yarn classic hosted pin requires the block's dependency sub-maps to match the patched package.json. If they don't, the patch is omitted with dependency_not_installed, or with dependency_manifest_unverifiable when the patched package.json can't be read (#1538, #1548).
    • Yarn berry workspace globs that use picomatch syntax are read the way yarn reads them: regex groups like packages/(a|b), ? and + quantifiers, POSIX classes and .. segments. A run from such a member now refuses with redirect_workspace_lockfile_elsewhere and names the root. Before, it pinned nothing and exited 0 (#1490, #1493).
    • vlt project roots and workspace members are found the way vlt finds them. That means the nearest ancestor vlt.json, extglob members (packages/+(a|b)), vlt 1.2/1.3 ! rules, and the members vlt 1.4 reads from a root pnpm-workspace.yaml. A hosted or vendored run from a directory that vlt installs from another root now refuses with redirect_workspace_lockfile_elsewhere / vendor_lockfile_missing and names that root. Before, it rewrote a vlt-lock.json vlt never reads while vendored vex attested not_affected (#1542, #1555, #1564, #1574, #1584, #1658).
    • Hosted vlt scans can skip a package resolved from a named or scoped registry, git, a remote tarball or a local folder. The skip warning now points to agent mode only, since vendored mode refuses those nodes too. It no longer comes with a duplicate redirect_vlt_entry_not_found "run vlt install" warning. The vendored refusal also names agent mode (#1601, #1677).
    • Vendored vlt refuses packages whose only build scripts are preprepare or postprepare (vendor_vlt_build_scripts_unsupported), including directories an earlier release already vendored. vlt ci runs those scripts inside the committed vendor directory (#1633, #1637).
    • pnpm 12.0–12.12 record a tarball: URL for packages from a scoped registry. rollback, remove and the hosted-to-vendored takeover now keep that URL, so the restored pnpm-lock.yaml matches what pnpm wrote (#1619, #1678).
    • Vendoring a pnpm package with an approved build script now copies the allowBuilds / onlyBuiltDependencies decision to the vendored name@file: key, which pnpm 10.34.2+ matches. pnpm no longer skips the script or fails frozen installs with ERR_PNPM_IGNORED_BUILDS. Revert, rollback and remove take the added keys out. The key is written only when the project's pnpm pins rule out releases older than 10.34.2. Otherwise, or when the value is flow-style, vendor_pnpm_build_approval_lost names the key to add (#1597, #1668).
    • Hosted pnpm reads pnpm_config_* variables the way pnpm does. On pnpm 11+, a pnpm_config_trust_lockfile set to anything other than true is the user's own setting. No trustLockfile: true is written for the variable to override, and the warning names the variable. A boolean variable that pnpm 11 ignores (an empty CI variable, null) is ignored, and pnpm 12's trimming is followed (#1514, #1533, #1557, #1575).
    • npm 12's allowScripts can approve a patched package's install script only by a version-pinned key, or deny a pinned version. That key stops matching once the lock points at the patch, so the next npm ci silently skipped the script. Hosted and vendored (package-lock.json) scans now warn redirect_npm_allow_scripts / vendor_npm_allow_scripts (#1595, #1657).
    • The vendored refusal of a lockfileVersion 4 package-lock.json now names npm install --lockfile-version 3. The old advice kept the lock at v4 and refused again forever (#1576, #1651).
    • Packages under the root project's own bundleDependencies are wired like any other lock entry. Before, hosted mode skipped them, vendored mode failed with exit 1 and vex left them out (#1618, #1661).
    • The npm-family crawler now walks the node_modules of declared workspace members under a directory named build, dist, vendor, tmp and the like. Agent apply patches them, and apply --check and vex no longer report an unpatched copy as patched (#1627, #1628).
    • A root hosted or vendored scan can meet an independent nested npm project, one with its own lock or installed with no lock. The scan now leaves that project's packages to their own run, with one nested_project_skipped warning naming the path to scan. Before, every vendored run failed with vendor_lock_entry_not_found and lost its in-run VEX (#1534, #1543, #1554, #1591).
    • Run from a vlt, pnpm or npm-linked workspace member, apply, rollback, apply --check and vex now follow the member's npm alias link ("lp": "npm:left-pad@1.3.0") into the root store. Before, they reported package_not_installed (#1494, #1500).
    • Vendoring into a pnpm or yarn berry project edits package.json in place instead of re-printing it. Only the pnpm.overrides, build-approval or resolutions members change; formatting, escapes, number spellings (5.173e28), duplicate keys, line endings and a BOM stay, so vendor then vendor --revert is byte-exact. A yarn berry revert removes a resolutions table only when vendoring created it and leaves one the project already had, even empty (#2052).
    • A yarn berry vendored revert reads package.json before it writes yarn.lock. A manifest that doesn't parse (a merge-conflict marker) now fails with "nothing was reverted" and both files untouched, instead of leaving the lock restored while resolutions still pointed at the vendored tarball (#2052).
    • A vendored pnpm or yarn revert finishes only once nothing resolves through the artifact. A pnpm revert points dependency references added after vendoring (a new package or workspace importer) back at the registry release, and refuses with vendor_lock_still_wired_revert_blocked, writing nothing, when one can't be restored; before, it deleted the artifact and pnpm failed with "Broken lockfile". Once a dependency was upgraded past the patched version, pnpm and yarn reverts remove the artifact and ledger entry with the new vendor_revert_dependency_moved_on warning instead of reporting the same drift forever (#2052).
    • Vendored pnpm no longer corrupts a packages: entry whose deprecated message is a block scalar, or one with a peer dependency named resolution. The rebuilt entry is read back before anything is written, and a lock that doesn't read back is refused with the project untouched (#2052).
    • Hosted pnpm keeps a CRLF pnpm-workspace.yaml CRLF when it adds trustLockfile: true. rollback restores the file byte for byte and removes a scaffold hosted mode created in either line ending (#2052).
    • Reverting two or more packages vendored into one bun.lockb restores the lock's exact pre-vendor bytes in any order, also for a ledger 5.0 / 5.1 wrote when one vendor --revert run reverts them all. Before, the first package's tarball path stayed in the lock while the revert reported success (#2052).
    • In a bun.lockb workspace, vendoring copies a tarball only into the members whose dependencies reach the package, instead of into every member's .socket/vendor/npm/<uuid>/. repair asks for the same members, and a revert still removes the copies an earlier release made (#2052).
    • A Bun vendored revert keeps the artifact and ledger entry (vendor_revert_residual_reference) while any bun.lock or bun.lockb entry still installs from it. Vendoring now refuses a bun.lock that lists the key it would rewrite more than once, as a merge that kept both sides leaves it (vendor_lockfile_version_unsupported); before, the revert restored one line and deleted the tarball the other still pointed at (#2052).
    • A vlt vendor --revert checks what it leaves behind. An unrecorded lock edge that still reaches the vendored node leaves the revert alone as drift, and anything else still naming the uuid directory keeps it with vendor_revert_residual_reference, instead of deleting the artifact with exit 0. vendor --revert --dry-run reports the same keeps as the real run. A workspace directory that is a symlink is refused (vendor_lock_entry_unsupported) instead of being written through, and a vlt-lock.json whose nodes or edges section can't be located for certain is refused before any write (vendor_lockfile_version_unsupported) (#2052).
    • The yarn_classic_workspace_member_install_risk warning says that a cold-cache install fails from every workspace member, whether or not it depends on a patched package, and names the install that works from a member directory: yarn install --cwd <workspace root> (#2052).
    • A broken project file is reported as a broken file. A package.json that isn't valid JSON now reads "not patched, because this project's own patches for it could not be checked", names the file and the parse error and says to fix it, instead of advising --allow-patch-conflicts. A package-lock.json that isn't JSON warns npm_lock_invalid and the scan ends "No packages found (see the warning)." instead of advising an install. Both runs still exit 0 (#2052).
    • vex reads a yarn berry lock that holds a YAML complex-key entry (? "<descriptors>", yarn's spelling of a key longer than 1024 characters). Before, the whole lock was rejected as "not a yarn lockfile" and no patch in it could be attested (#2052).
  • PyPI.
    • A requirements line pinned through an environment variable (six==${SIXV}) is refused by vendor with pypi_requirement_env_templated, and vendor --check and vex read it as possibly installing any release, so the patch is no longer attested while pip install -r can install the unpatched one (#1873, #1874).
    • A hosted scan of a Python project with no lockfile the rewriters read now warns redirect_pypi_no_lockfile, naming the files looked for and the project's own lock command, instead of passing silently (#638, #1885).
    • After a hosted rollback, a vendored rollback or vendor --revert of a requirements-file pin, the reinstall advisory covers pip and names the requirements files to reinstall from (#1953, #1954).
    • The hosted Pipenv stale-install warning ends with the same runnable remedy as vendored mode and never prints an option-like package name into a shell command (#1878, #1883).
    • A poetry.lock in an old format (0, 1.0, 1.1) keeps its "written by Poetry < 1.4" warnings and refusals even when a hand-merged header claims a newer Poetry (#1847, #1865).
    • Hosted rollback, remove and scan restore the pins hosted mode writes into uv 0.2 [[distribution]] locks (uv < 0.2.35) instead of refusing them (#2014).
    • Vendored dry runs and rollout planning apply the PyPI pre-build refusals the wet run gives (pypi_uv_no_sources, pypi_no_requirements, symlinked script and pylock.<name>.toml files), so a refused patch shows as would-refuse and no longer takes a --max-new-patches slot (#891, #1813, #1908, #1930).
    • A pip project with both requirements.txt and the pylock.toml pip lock wrote from it now has the exact requirements.txt pin (and in-root -r includes) wired to the vendored wheel too, so a hosted -> vendored switch no longer leaves pip install -r unpatched. A fresh wiring where requirements.txt pins the package with extras is refused (pypi_extras_unsupported), and a pin only in an out-of-root -r include refuses with pypi_requirements_outside_root and blocks vendor --check / vex (#1758, #1759, #1794, #1796, #1851, #1853).
    • A vendored requirements line whose # socket-patch vendor: comment was respaced, extended or stripped still reverts, and a re-run no longer appends a duplicate line (#977, #1825).
    • A pip marker split whose branches live in different -r files (six==1.16.0 ; python_version < "3.12" here, six==1.17.0 ; ... there) vendors instead of failing with "not pinned" (#1104, #1703).
    • When a pip file reads an edited requirements file as constraints (-c requirements.txt), hosted and vendored runs warn redirect_requirements_constraints_link / vendor_requirements_constraints_link: pip 20.3-21.0 reject link pins in constraints (#740, #1726).
    • Vendored Pipenv warns pypi_pipenv_hash_export_unsupported when pipenv requirements --hash would drop the vendored wheel's hash and make pip refuse the whole install (#981, #1731).
    • Vendored PDM projects with a pre-2.11 pdm.lock warn pypi_pdm_stale_install with the right reinstall remedy when the env already holds the unpatched release (#641, #1727).
    • A Pipfile [pipenv] venv_in_project = true with no ./.venv no longer hides the WORKON_HOME venv older Pipenv uses (#842, #1729); a lockless uv project with a Poetry plugin table no longer skips its UV_PROJECT_ENVIRONMENT (#1916, #1922).
    • scan --mode vendored / vendor over a hosted pin at an older patch now vendors the newer patch instead of exiting 1 with "no installed package found on disk" (#1105, #1713).
    • Hosted mode refuses pins it can't restore or that uv would bypass: a uv project with dynamic = ["dependencies"] (redirect_uv_metadata_unsupported) (#639, #1709), and a Hatch env using the uv installer whose [tool.uv.sources] shadows the pin (redirect_hatch_unsupported) (#673, #1706). Vendored mode refuses a Hatch 1.16+ workspace member (pypi_hatch_unsupported) instead of breaking the workspace root env (#505, #1724).
    • Agent apply and rollback delete the __pycache__ bytecode compiled from each .py they rewrite, so a same-size patch in the same second as the install no longer keeps the old code running; a .pyc that can't be deleted warns stale_bytecode_not_removed (#819, #1722).
    • uv installs a package covered by no-binary or no-binary-package (in the uv settings, or on the uv pip compile line in a pylock header) only from an sdist, and every Socket patch is a wheel. Hosted and vendored scans now refuse such packages before writing. Before, the next uv sync / uv pip sync failed. Vendored mode uses the new pypi_uv_no_binary, and hosted mode its uv unsupported warnings (#1603, #1649, #1672).
    • uv settings inherited from a parent directory are honoured. A uv project with no [tool.uv] of its own, below a parent uv.toml or [tool.uv] whose settings the wiring would hide, is refused. Vendored mode uses the new pypi_uv_ancestor_settings and hosted mode redirect_uv_project_unsupported. A PEP 723 script below a parent that sets no-sources is refused, where before it was wired and exited 0 (#1501, #1504, #1545, #1566).
    • Hosted uv rollback and remove refuse to restore a lock entry whose files uv filtered, instead of writing back every file of the release. Filters include a per-package exclude-newer-package cutoff, and a uv pip compile pylock built with --no-build, --only-binary, --no-binary, an exclude-newer cutoff, or flags it doesn't record (#1604, #1613, #1614).
    • Hosted uv scans warn redirect_uv_override_requires_uv_0_5_6 / redirect_uv_constraints_require_uv_0_5_6 when they wire overrides or repoint [manifest] constraints that old uv ignores, as vendored mode already did (#1569, #1609).
    • Hosted uv rollback drops the bare ==<version> [manifest] overrides record that uv before 0.5.6 left for the hosted pin (#1525).
    • Reverting one of several vendored packages in a pip lock pylock.toml keeps its wheel hashes table under its own package. Before, the table landed after the next [[packages]] header and pip refused the lock (#1623, #1625).
    • Vendoring and reverting a requirements.txt that mixes CRLF and LF lines keeps each line's own ending, so vendor --revert and remove restore the file byte for byte (#1567, #1612).
    • A vendored PyPI entry wired across flavors (for example requirements.txt, then a pylock.toml) is reverted record by record through each owning backend. Before, vendor --revert, remove and the takeover exited 0 and restored nothing (#1524, #1526).
    • When Poetry re-locks a vendored unit, for example with another Poetry version, vendor --revert, rollback, remove and the takeover restore it while it still carries exactly the wiring socket-patch wrote. Before, they reported drift forever (#1615, #1662).
    • A source = "pypi" pin on a patched package over a lock-version 1.0 or 1.1 poetry.lock is refused by hosted, vendored and the takeover. Before, it was rewired with exit 0, and every Poetry 1.3+ then failed to install it (#1503, #1506).
    • Pipenv locks whose categories pin different versions of a package are patched: only the categories that pin the patched release are rewired. Before, hosted mode switched nothing and vendored mode failed with pypi_pipenv_version_mismatch (#1616, #1617).
    • The Pipenv stale base-interpreter remedy exports comma-joined lock categories (default,docs). Before, Pipenv 2022.12–2023.2.4 read the export as empty, so pip did nothing (#1507, #1508).
    • vex and vendor --check treat more requirements.txt and Pipfile.lock sources as installing the upstream release, which contests a Socket wiring in another lock. These are any exact pin spelling (===1.2.3, ==1.2.3.0, ==v1.2.3), bare wheel or sdist paths (wheels/six-1.16.0-py3-none-any.whl), direct URL and path references, and editable or #egg= VCS lines. Such a wiring now gets patched_ref_unattributable instead of an attestation (#1549, #1562, #1587, #1599, #1602, #1632).
    • The VEX product taken from pyproject.toml now uses the PEP 503 name and PEP 440 version that SBOMs record (my-service@1.0.0rc1, not My_Service@1.0.0-RC1). Before, consumers silently ignored every statement. Poetry 2 ([project] name, [tool.poetry] version) and PDM 0.x ([tool.pdm]) layouts are detected instead of failing with product_undetected (#642, #1527, #1529, #1547, #1577, #1589, #1653).
    • Hosted rollback restores a uv.lock whose only registry package is the patched one (a project with one dependency) instead of failing with "no sibling registry package". The entry is rebuilt on PyPI's default index with the fields uv writes at the lock's revision, and the run warns upstream_uv_entry_rebuilt. It still refuses when the revision is newer than 3 or uv is configured with a package index (#2052).
    • A patched wheel's Requires-Dist / Provides-Extra headers reach uv.lock only as TOML strings. A header holding a double quote could add a key to the lock or leave it unparseable while vendored uv exited 0. A header a lock entry can't hold now refuses the wheel before any file is written (pypi_uv_metadata_unsupported; pypi_lock_parse_failed for a vendored script lock; hosted mode skips with python_metadata_unavailable). That includes the direct references (name @ url) vendored mode used to wire while leaving a lock uv lock --check calls stale (#2052).
    • Vendored uv keeps each line's own ending in a pyproject.toml that mixes CRLF and LF, instead of rewriting the file all-LF, and vendor --revert restores it byte for byte (#2052).
  • Go.
    • In a go 1.16 (unpruned) module where a dependency imports a fork that the root go.mod replaces with the patched module, apply, apply --check, hosted mode and vex read the module graph to tell whether the patch is really built, instead of reporting it applied while go build linked the unpatched code (#1840, #1843).
    • In a go 1.16-or-older module where a downgrade left the old version's go.sum lines behind, apply, apply --check, hosted mode and vex use the version the module graph selects instead of the highest go.sum line. When the module cache can't answer, they fail closed: apply refuses with a remedy, apply --check reports drift, and the patch is not pinned or attested (#1862, #1893).
    • vex reads the go.work that governs the project (GOWORK, else the nearest one at or above it, with members anywhere), as hosted and vendored modes already did, and no longer attests a Socket replace the workspace keeps out of the build (go_workspace_override). Such a replace stays visible to list, rollback and remove, and a root go.work replace that go never reads no longer withholds the root go.mod's copy of it (#2011, #2031).
    • The in-memory hosted engine judges a Go workspace from the project's own files instead of probing the process's working directory, so a root module the go.work does not use is no longer refused for a missing go.mod (or answered from the server's) (#2031).
    • Go workspaces are read as go reads them: GOWORK=auto isn't a file name, a relative GOWORK isn't in effect, the built version comes from all members' requirements, and go's replace precedence applies; an unreadable member go.mod means nothing is attested. Hosted scans check go.work before writing and skip a replace it would override, ignore or reject (redirect_golang_workspace_conflict), and rollback restores hosted pins in go.work, go.work.sum and member go.mod files (#1763).
    • When another go.work member requires a newer version, apply and vendor refuse, apply --check fails, vex omits the patch (go_version_not_selected) and hosted refuses the pin (redirect_golang_version_mismatch), removing one written earlier (#1751, #1752, #1753, #1755).
    • After go mod tidy drops the Socket module's zip line, vex still recognises a hosted pin from its /go.mod go.sum line when the module graph selects it, and a hosted re-run restores the zip line (#1748).
    • A -modfile in GOFLAGS naming another file is treated like a go.work override: nothing is written, apply --check and vendor --check fail (go_workspace_replace_ineffective), and vex omits the patches (go_workspace_override) (#1799, #1800). With GOWORK naming a sibling file, the replace no longer goes into the unread ./go.work (#1810, #1812).
    • A module the build uses only as the target of another module's replace is refused by apply and vendor, flagged by apply --check, omitted by vex, and refused by hosted mode (redirect_golang_not_in_module_graph) (#1823, #1824).
    • Undoing a Go patch (vendor --revert, rollback, remove, including in a go.work member) while a committed vendor/modules.txt still records the replace warns go_vendor_modules_txt_out_of_sync naming go mod vendor / go work vendor, and vendor --check fails on it (#1766, #1767, #1777, #1778).
    • A global apply warns go_installed_tool_stale for each go install binary built from a patched module before the patch, naming the rebuild command, and vex -g omits the patch until it is rebuilt (#422, #1717).
    • GOMODCACHE / GOPATH are resolved as go does (first GOPATH entry only, relative or ~ entries refused), so the crawler no longer patches a module cache go never reads (#1834, #1876).
    • Go delivery hardening: vex and apply --check check the vendor/ tree go selects before attesting; upstream go.sum lines are kept and restored on rollback after go mod tidy, conflicting checksums are refused, and after the module cache is cleared a re-apply reuses the verified project-local copy and repairs a missing root go.mod mirror (#1698).
  • Cargo.
    • vex reads workspace member manifests: a Cargo pin that another root or member declaration resolves from crates.io (or another source) is withheld (vex_cargo_unpinned_declaration), as is a vendored copy a member's other registry, git source or stale pin bypasses (vex_cargo_patch_bypassed); [patch] entries cargo shadows or canonicalizes are weighed the way cargo does (#2012).
    • Hosted Cargo pins dotted-key (serde.version = "1.0.0"), literal-string (serde = '1.0.0') and multi-line inline-table dependencies instead of refusing them with redirect_cargo_toml_dep_unrewritable; rollback restores the manifest byte for byte (#757, #1735, #1743).
    • Agent apply and vex ignore a registry-cache copy of a crate that Cargo.lock takes from git, a [patch] path or a [replace], which cargo never builds (#506, #1715).
    • Workspace member globs with character classes (crates/[ab]*) expand as cargo's glob crate does (#1833, #1866).
    • With HOME and CARGO_HOME unset, the cargo home is found from the passwd entry as cargo does (#1135, #1779), and the build-cache cleaner reads the .cargo/config chain as cargo does, including from a member or a relative --cwd (#1832, #1849).
    • Hosted scan refuses Cargo patches with redirect_cargo_old_cargo when rust-version or rust-toolchain[.toml] declares a cargo older than 1.68, which can't use sparse registries, and points to --mode vendored. Before, it reported success and every build failed. rust-version.workspace = true is followed to [workspace.package], so vendored mode no longer warns that a standard workspace declares no rust-version (#651, #653, #1429).
    • rollback and remove of a hosted Cargo pin no longer panic (exit 101) on a Cargo.lock package whose name is empty or not ASCII, or on a v1 lock that lists the hosted block twice. A pin with such a block is refused whole ("is not a crates.io crate name") before any lookup, with no file changed (#2052).
  • JVM.
    • A Maven 4 root that builds its subdirectories implicitly (model 4.1.0, pom packaging, no <subprojects>) is treated as a reactor by hosted mode, vex and the Maven cache scope, not only by vendored mode, so a root-only pin is no longer attested while a module keeps its own version (#1809, #1886).
    • Hosted Maven keeps every line of .mvn/checksums/checksums.sha256 it doesn't own (single-space or * sha256sum lines, tabs, comments) and the file's line endings, instead of silently dropping them (#2016).
    • Vendored Maven vex no longer attests a build that may still use the unpatched jar: an executable classifier copy of the patched artifact, or a suffixed version left only in a plugin, profile or unused property, now withholds attestation (#2006).
    • Vendored Gradle withholds attestation when a gradle.lockfile locks the patched artifact at another version (gradle_lock_overrides_vendored) or a custom lockFile location is used (gradle_custom_lock_file) (#2004).
    • sbt: vex no longer attests a hosted pin that a build reassignment of dependencyOverrides or resolvers, sbt.override.build.repos=true, or newer resolution evidence shows the build doesn't apply (vex_sbt_pin_bypassed) (#2030), nor a vendored pin without current sbt resolution evidence or with its vendored tree missing (vex_sbt_resolution_unverified, vex_sbt_vendored_tree_missing) (#2008).
    • vex and hosted mode weigh profiles, properties and coordinates the way Maven resolves them: a <profiles> declaration, profile <properties>, .mvn/maven.config user properties, nested ${a}/${b} properties, or a profile that redefines a groupId/artifactId property now withhold attestation (vex_maven_profile_unpatched), and hosted writes no pin a profile version or an executable classifier copy bypasses (redirect_maven_profile_dependency_unpatched, redirect_maven_classifier_unsupported) (#2005).
    • ${property} groupId / artifactId are resolved: hosted pins the real declaration and refuses an undefined one (redirect_maven_coordinate_unresolved), and the vendored planner, check and revert match interpolated coordinates (#655, #683, #1773, #1788).
    • Vendored reactors leave a root unpinned when a module's version is an undefined or profile-moved ${property} (#459, #513, #1787), and a plan that wires nothing is refused (vendor_jvm_shape_unsupported, reason nothing_wired) instead of exiting 0 (#513, #1826).
    • .mvn/maven.config is read one argument per line as Maven 3.9+ does: commented lines are skipped and --define=k=v / -D k=v count (#535, #550, #1754).
    • Vendoring a Maven 4 root covers its implicit subprojects (#622, #1807); vex withholds a reactor where a module is left at the base version (#584, #1802); a parent <relativePath> naming a directory no longer fails vex and vendor --check (#534, #1789).
    • Agent mode and -g find the local repository as Maven does (MAVEN_REPO_LOCAL, -Dmaven.repo.local, <localRepository> in settings, then user.home), not $M2_HOME/repository (#423, #1801).
    • A stray file in a vendored Gradle or Maven tree (such as .DS_Store) counts as drift: vendor deletes it and repair removes it offline, instead of reporting already_vendored while the build fails (#656, #1819).
    • A vendored Maven pom resolves what its upstream pom does. When a parent pom manages siblings at ${project.version} (log4j up to 2.17, logback), the suffixed pom resolved them at <base>-socket.<hex8>, which nothing serves: vendor and vendor --check reported success and the build failed. The vendored pom now declares what it inherits at the base version, between socket-patch:inherited markers. vendor --check now fails on a tree 5.0 / 5.1 wrote with that defect when a local Maven or Gradle cache holds the parent poms, naming the dependency and the parent, and vendor rewrites it. Shapes the pom can't express refuse with inherited_version_unresolvable, and a first vendoring whose parent poms can't be read with parent_unavailable. A parent or BOM that only a corporate repository holds is read from the local repository instead of leaving the root unpinned (#2052).
    • Vendoring keeps the bytes of a .mvn/maven.config that is not UTF-8 (a Latin-1 -Duser.name=José) instead of rewriting them as U+FFFD, and the revert restores the file instead of leaving the added lines behind (#2052).
    • A Gradle module declared with a computed classifier (${osdetector.classifier}) is refused: by vendored mode with the new gradle_classifier_computed, by hosted mode with redirect_gradle_classifier_declared. Before, vendor and vendor --check reported success while Gradle could no longer resolve the classifier jar (#2052).
    • vendor --revert finds its gradle/verification-metadata.xml entry after the file was re-serialised (./gradlew --write-verification-metadata) and puts the original entry back; before, it reported success and left the patched jar's sha256 in place. When the patched hash sits where no recorded shape applies, nothing is reverted and vendor_lock_entry_drifted names the entry (#2052).
    • In a settings.gradle.kts, vendoring creates pluginManagement {} after the file annotations (@file:Suppress(…)) instead of above them, where the script stopped compiling (#2052).
    • The Gradle and Maven wrapper version is read from the distributionUrl = … and distributionUrl: … spellings too, so a Gradle 6.7 wrapper no longer passes vendored mode's 6.8 gate and a Maven 3.9.9 wrapper no longer reads as unknown (#2052).
  • NuGet.
    • When a packages.lock.json locks a patched package at a version the writers refuse to wire (another target framework or project), vex no longer attests that version (vex_nuget_lock_other_version); 1.0 and 1.0.0 count as one version (#2007).
    • Agent mode patches the packages folder the project restores into (globalPackagesFolder, RestorePackagesPath, repositoryPath, obj/project.assets.json folders) instead of the default ~/.nuget/packages (#397, #398, #1769), and -g runs leave a vendored project's own install alone (vendored_ownership_retained) (#489, #1790).
    • rollback no longer panics on a package id with non-ASCII letters (Ünicode.Pkg) when nuget.config maps package sources by prefix pattern (#2052).
  • Composer.
    • A composer.lock with a UTF-8 BOM is read everywhere, and vendor then --revert keeps the BOM byte for byte; a revert no longer deletes a vendored artifact such a lock still installs from (#2017).
    • On Composer 1, packages composer/installers places elsewhere (WordPress plugins, Drupal modules, installer-paths) are found (#463, #1723).
    • Hosted scan and get take over a vendored Composer package by reverting it first, and leave an unledgered vendored entry alone with redirect_composer_vendored_entry (#536, #1725).
  • Vendoring.
    • Vendored dry runs leave already-vendored patches out of the "Would download and vendor" headline and list them as skipped, matching the wet run and the JSON (#1850, #1856).
    • Run from a workspace member with no lockfile of its own, a vendored scan, get or vendor names the lock that installs the member and where to run from (#1904, #1913).
    • npm package-lock packages installed from a URL or git spec, and workspace members, are refused before their patch is downloaded; dry runs preview them as refused and they no longer hold a --max-new-patches rollout slot (#1976, #1977).
    • In a vlt project, vendor no longer requests service downloads for packages it then refuses (#1909, #1947).
    • Every vendored entry point (scan previews and rollout plans, the download phase, get, and vendor) applies the same refusals before any download: a v1 package-lock.json project, or a hosted pnpm pin whose takeover would be refused, is refused before its patch is fetched; get <uuid> --mode vendored reports every such refusal and writes nothing; and vendor refuses before restoring a hosted pin instead of restoring it and rolling back. Codes and details are unchanged (#2015).
    • A vendored revert keeps an artifact the lock still references through an escaped path spelling (\/ in an older composer.lock, Windows backslashes) instead of deleting it (#2022).
    • The committed vendor ledger can only undo vendoring's own edits. A tampered .socket/vendor/state.json, which a pull request can carry, could make vendor --revert, remove, rollback, scan --prune or a takeover write .git/config, a CI workflow or any root file, with exit 0. A record may now name only the files its backend edits, never a path under .git or through a symlink, and may put back only the pre-vendor form of the package's own entry. A record that doesn't fit fails that package's revert (exit 1) with nothing written for it, and the other packages still revert. nuget.config snapshots are recorded without credentials (ones an earlier release recorded are dropped at the next save but stay in git history, so rotate them), and vendored gems refuse a gem line whose positional argument is computed (gemfile_declaration_not_editable), since it couldn't be reverted (#2052).
    • Finishing an interrupted vendored commit can no longer write under .git, and is never silent. A checkout can carry a hand-written .socket/vendor/.commit-journal.json; one that names a path inside a git directory, in any spelling a filesystem folds onto .git, is now set aside with nothing applied, like one naming an absolute or .. path. A replay prints the files it rolled forward on stderr and adds a vendor_commit_recovered (or vendor_commit_set_aside) warning under --json. A --dry-run of apply, rollback, remove, repair or vendor no longer replays a pending journal: it exits 1 with the new vendor_commit_pending error, where it used to finish the commit and then preview (#2052).
  • General.
    • Workspace runs never read or rewrite project files outside the enclosing git repository root. Ancestor workspace roots, settings files, go.work, NuGet parent configs, .cargo/config and out-of-tree requirements above the root are not read: writers refuse (new redirect_cargo_config_outside_root, redirect_nuget_inherited_config_outside_root, pypi_requirements_outside_root) and vex withholds. Vendored writers and vendor --revert only claim .socket/vendor/ paths anchored at this project, so a sibling checkout's wiring is never rewritten. Global runs (-g, --global-prefix) and projects outside a git checkout are unchanged (#2025).
    • The nested-project remedy quotes its path for the shell it prints for, including Windows and % (#1859, #1884).
    • Patch and vendor download errors read the same on every path (size limit, HTTP status with a bounded body excerpt), and artifact downloads log under --debug (#1869, #1870).
    • Vendored re-runs in every ecosystem keep the older vendored patch (vendor_prebuilt_pending / vendor_prebuilt_unavailable) while a newer one has no prebuilt artifact, instead of exiting 1 (#1235, #1707).
    • apply, apply --check, vendor, repair, remove and rollback refuse with manifest_unreadable when the manifest path can't be read (a .socket file, a symlink loop, a directory), instead of treating it as no manifest, and scan's GC passes warn (#998, #1063, #1123, #1776).
    • vendor, vendor --revert and vendor --check run from an npm-family workspace member with no lock of its own refuse with vendor_lockfile_missing (exit 1) and name the workspace root to run from, instead of exiting 0 having checked nothing (#1921, #1928).
    • An unscoped rollback over lockfile wiring the vendor ledger doesn't own (a git merge kept one side of state.json) refuses with vendor_ledger_missing and writes nothing (#991, #1739).
    • rollback in the wrong scope keeps a record whose copy in the other scope is still patched and warns patched_in_other_scope instead of dropping it (#450, #1714).
    • An interrupted vendor eject (Ctrl-C, CI cancel) leaves the project hosted, or rolls forward on the next run, instead of leaving the lock on the unpatched registry tarball (#1782, #1783).
    • When the public proxy rejects a batch search, the per-package fallback honours the proxy cap and SOCKET_API_CONCURRENCY instead of sending 10 requests at once (#614, #1817).
    • vex no longer attests a pre-v5 ledger record wired through a hosted-shaped URL on a patch server the run was not given (staging without --patch-server-url, look-alike hosts): it reports redirect_unwired with a vex_claim_unwired note (#2012).
    • socket-patch's records are no longer lost silently to a .gitignore rule. Vendored runs, including the hosted-to-vendored eject, refuse before writing with vendor_ledger_gitignored when git would ignore .socket/vendor/state.json. They refuse with vendor_artifact_gitignored when a .socket/ or .socket/vendor/ rule hides a gem, PyPI, Cargo, Go or Composer artifact. Agent scan and get warn manifest_gitignored for an ignored .socket/manifest.json. Files git already tracks don't count (#1638, #1640).
    • Vendored artifacts now survive .gitignore and Git LFS rules. The npm-family uuid .gitattributes also unsets filter, ident and working-tree-encoding, so *.tgz filter=lfs no longer commits LFS pointers. Vendored PyPI wheels get the same .gitignore / .gitattributes pair. repair rewrites both files, and it fails with vendor_artifact_gitignored when a rule they can't override hides the artifact (#1541, #1559, #1572, #1581, #1593, #1656).
    • Lockfile and manifest rewrites (hosted, vendored, rollback, remove, vendor --revert) keep the file's owner and group. A run as root in a Docker bind mount or under sudo no longer leaves poetry.lock owned by root (#1635, #1636).
    • socket-patch --update and the update notice now name the package manager that installed the CLI. Bun installs get bun add -g, bun add or bunx. Yarn installs get yarn global add (also for a global folder moved with global-folder), yarn add / yarn upgrade in yarn 1, or yarn up in yarn 2+. Before, the npm command they got left the copy stale or wrote a stray package-lock.json (#1621, #1622, #1629, #1680, #1696).
    • Revert drift warnings print ledger keys plainly (default:six, requirements.txt:1, <unknown>) instead of Some("default:six"), and escape control characters from the committed state.json (#1608, #1679).
    • Failed upstream registry lookups and downloads now name the transport cause (refused, reset, DNS, TLS, timeout), with URLs redacted. Before, they said only "error sending request" (#1650, #1652).
    • A global scan --prune / --sync (--global, --global-prefix) keeps the manifest entries and blobs of the --cwd project. Before, it judged the project's manifest against the global crawl alone, emptied it and deleted .socket/blobs/ with exit 0. A pruned manifest that can't be written now prunes and sweeps nothing (manifest_write_failed), and the blob, diff and package sweeps of repair, remove, rollback and scan --prune refuse a symlinked .socket/ level (cleanup_failed) instead of deleting the files at the link's target (#2052).
    • Hosted mode checks each patch reference the API grants before any of it is written into project files: a canonical uuid, https URLs of plain URL characters, digests of the right length and alphabet, names and versions in their ecosystem's grammar. A grant that fails is skipped with the new redirect_grant_malformed (the detail names the field), and a plain http URL anywhere but the --patch-server-url origin or a loopback address with redirect_grant_insecure_url; nothing is written for that package and the exit code is unchanged. Before, a quote, newline or markup in a grant could add fields to yarn.lock or Gemfile.lock, break pom.xml or panic the run (#2052).
    • Hosted multi-file writes are all or nothing. rollback, remove and the withdrawn-pin sweep check every file a restore would write before touching the first, and put back the files already written when a later write fails. Before, a symlinked pom.xml left .mvn/maven.config deleted and the pin live while rollback printed "Restored" and counted it. Such a pin is never reported restored now: rollback emits hosted_write_failed and exits 1, and --dry-run previews that failure with exit 1 where it previewed a success. A hosted scan whose write fails says "(nothing was changed)" only when every file was put back, and otherwise names each file and the state it is left in (#2052).
    • Agent apply and rollback of a package are all or nothing and keep file modes. Every file is staged with its final mode and owner before the first is renamed into place, and a failure puts the files already replaced back, so a corrupt blob or EACCES on a later file no longer leaves the earlier ones patched, and an interrupt no longer leaves a 0755 script at 0644. Errors name the file (Cannot apply patch: <file> - <cause>). A killed run can leave .socket-stage-* / .socket-orig-* files beside the package's files; the next apply or rollback of that package finishes it and removes them once they are ten minutes old. When a failed write can't put a file back either, its original is kept beside it as .socket-kept-<file>-<uuid>, which no later run removes (#2052).
    • Commands that rewrite .socket/manifest.json (remove, rollback, get, scan) keep the top-level and per-record fields this version doesn't know, such as ones a newer CLI wrote, and the file's mode. Before, the first rewrite dropped them and a 0600 manifest came back 0644 (#2052).
    • apply, rollback, remove and repair set up the API client only when they have something to download. A run that finds everything on disk makes no API request, prints no "No SOCKET_API_TOKEN set" notice and reports no api_auth_fallback. A no-op apply with a token and no --org used to wait on the organization lookup first: 12 s against an endpoint that drops packets, 65 s against one that never answers. An apply with a Maven patch in scope still sets the client up (#2052).
    • Telemetry opt-outs that did nothing are honoured: DO_NOT_TRACK (any value except 0, false and empty) and SOCKET_PATCH_TELEMETRY_DISABLED, the v3/v4 name that v5 had dropped, now turn telemetry off like SOCKET_TELEMETRY_DISABLED. README and docs/configuration.md gain a Telemetry section listing every event, field and switch, and the --no-telemetry help no longer calls the events anonymous (#2052).
    • A closed output pipe no longer cuts a command short. apply | head -1 or rollback --yes | true used to die of SIGPIPE (exit 141) at the line being printed, with .socket/apply.lock left behind and, for rollback and remove, possibly between the restores and the manifest cleanup. The command now stops printing, finishes its work, releases the lock and exits with its own code; a read-only command piped into head exits 0 (#2052).
    • Staging entries an interrupted run leaves under .socket/ are swept: every lock-taking command except a dry run removes .socket-stage-* entries older than an hour. apply now stages the blobs it downloads in .socket/.socket-stage-apply-blobs-*, removed when the run ends, instead of the system temp directory, so it no longer needs a usable $TMPDIR and a killed run no longer leaves a copy of the blob cache there (#2052).
    • apply --force names the patch files it skipped because they are missing on disk (new patch_files_skipped warning), and a re-run that writes nothing reports already_patched instead of an applied event with no files (#2052).
    • apply, apply --check and list --json warn vendor_state_unreadable when .socket/vendor/state.json can't be read, instead of silently treating vendored packages as in-place patches or leaving them out of a success listing. Exit codes are unchanged (#2052).
    • gem env and the python3 probes run from --cwd for a project and from the home directory under --global, not from wherever the CLI was started. A version manager's shim (rbenv, pyenv, asdf, mise) therefore picks the project's interpreter, and the directory the shell is in no longer steers a global scan (#2052).
    • remove and rollback no longer promise a later restore for a patch whose package isn't installed. The warning and the rollback_not_installed event now say that nothing was restored, that the record is gone, and that reinstalling the package is the remedy (#2052).
    • --help wraps to the terminal (100 columns at most) instead of running lines of up to 564 characters, and the root help defines hosted, vendored and agent mode. The npm launcher explains a missing platform package (npm install --omit=optional) and how to get the binary instead of printing spawnSync … ENOENT, and lists the supported platforms on an unsupported one (#2052).
    • In a project with no patch of any mode, rollback, repair, remove and vex say "No patches in this project; nothing to roll back." (or to repair, to remove, to attest) instead of "Manifest not found at ./.socket/manifest.json", a file a hosted project never has. Exit codes and the --json manifest_not_found error are unchanged (#2052).
    • When vex can't detect a product, the error lists what detection reads, names the project files that lack a usable name and version, and ends with the run's own flag and a value to paste (--product pkg:generic/<directory name>, or --vex-product on an embedded run). It still exits 2 with product_undetected (#2052).
    • --product / --vex-product and --doc-id / --vex-doc-id values are emitted trimmed; surrounding whitespace used to land in the document's @id. A document id with no IRI scheme is honoured with a new doc_id_not_iri warning (#2052).
    • A report-only scan (--prune, --global, --global-prefix) previews as the report it is: its --dry-run no longer prints "Would download and apply N patches" for a run that applies nothing, and the listing is headed Patches available: (#2052).
    • The hosted summary counts only the packages the run switched and gives the others a sentence of their own: "Switched 3 packages to hosted patches; rewrote 2 files. 1 package was already on hosted patches." --json is unchanged (#2052).
    • Crafted project files no longer hang, exhaust memory or crash a run: a workspace glob with many *, **, brace sets or extglob groups (a pattern past the bounds fails closed and lists every directory); a Hatch matrix (read up to 1,024 combinations); a chain of Maven ${property} references (a value past the bound reads as unresolved); a Gradle string nested more than 64 templates deep (the script is refused as unparseable, where scan aborted with exit 134); a small crafted bun.lockb that made vendor allocate gigabytes; and a torn bun.lockb snapshot in the vendor ledger, which vendor --revert now leaves alone as drift instead of panicking (#2052).
    • The git checkout remedy printed when a hosted pin can't be restored names every file hosted mode edited: Gemfile with Gemfile.lock, and the pyproject.toml or script paired with a uv.lock (#2052).

Performance

  • npm family.
    • Bun scans parse each bun.lock once instead of three times, and the vendored preflight no longer parses it once per patched package (#1606, #1700).
    • Hosted vlt scans index vlt-lock.json once instead of re-parsing it per patched package: about 47% (hosted) and 50% (rescan) faster on the bench (#1570, #1691).
    • Rescans of already-patched bun and yarn berry projects drop repeated per-package lookups that had made them about 20% slower since early October (#1606, #1607, #1693).
    • Reverting a vendored pnpm package that much of the lock depends on reads the lock section once instead of once per dependent. On a 60,000-package lock a revert took 1 min 51 s against 0.77 s to vendor; a revert over 20,000 dependents drops from 94 s to about half a second in a debug build (#2052).
    • Vendoring into a large vlt-lock.json no longer decodes lock edges for every comparison. It took 20.9 s for 24 packages on a 20,000-node lock; planning one package drops from 2.1 s to 12 ms in a debug build (#2052).
  • PyPI.
    • Hosted Poetry and PDM scans parse each lock once instead of five or six times (about 31–34% fewer instructions on the bench). VEX and the lock inventory read Python locks with a read-only TOML parse instead of copying them (#1473, #1624, #1684, #1486).
  • Go.
    • Hosted Go rescans parse go.sum once per line instead of re-tokenizing it for every patched dependency, recovering the ~30% slowdown on large go.sum files (#1424, #1746).
  • JVM.
    • A Maven pom whose declarations share ${property} texts is interpolated once per profile combination instead of once per declaration. A 1,000-declaration pom that cost 263 million property lookups, 28 s in socket-patch list, now takes under 600,000 units of work (#2052).
  • General.
    • The project-patch check and purl canonicalization are cheaper on large Yarn Berry locks, about 3% fewer instructions per rescan (#1607, #1701).
    • Hosted rescans no longer ask the patch API's reference endpoint twice about the same pinned patches (#1606, #1607, #1695).
    • Agent apply and rollback flush the device cache twice per package instead of twice per file. On a 2,000-file package on macOS, apply drops from 16.4–18.8 s to 1.2–1.6 s and rollback from 17.5–18.2 s to 2.0–2.8 s (#2052).
    • Telemetry never delays a command. Events are sent in the background and the exit waits at most 300 ms for them. With an endpoint that accepts and never answers, list drops from 5.02 s to 0.32 s and prints its output after 0.02 s instead of 5.02 s. An event still in flight after the wait is dropped (#2052).

Maintenance

  • Code consolidation with no output change: one lock-rewrite driver for Poetry and PDM (#1898) and module sweeps across the CLI and core (#1855, #1864, #1875, #1884, #1892, #1897, #1903, #1907, #1914, #1920, #1923, #1929, #1939, #1948, #1967, #1975, #1985, #1990).
  • Test and CI infrastructure: shared test env guard, docker harness, PTY runner, copy_tree and apply fixtures; hermetic children scrub color variables; failing tests are annotated in CI; offline bun.lockb rollback mock and a Windows venv layout in the PyPI fixture (#1891, #1890, #1905, #1944, #1955, #1964, #1972, #1978, #1988, #1994, #2000); release workflow artifact steps move off Node 20 actions (#2027). A design doc and line-count budget plan one lock model for every lockfile reader (#2001), and the Socket-origin classifier moves into one module (#2002). Test fixes: PDM e2e compat pins bounded by release, a Windows-only scala-cli liveness test, NuGet walk tests isolated from the host cache (#2034, #2035, #2037); CI retries setup-ruby and setup-deno once on a release-CDN error (#2041, #2042), lint-ecosystems installs retry release 504s, and vexctl is cached on every OS (#2040, #2039).
  • Other CI, test, refactor and dependency work with no change in behavior: #1488, #1491, #1495, #1497, #1498, #1499, #1502, #1505, #1510, #1515, #1518, #1519, #1522, #1528, #1530, #1532, #1535, #1536, #1539, #1544, #1546, #1550, #1552, #1553, #1556, #1560, #1563, #1565, #1568, #1580, #1583, #1585, #1590, #1620, #1634, #1641, #1642, #1643, #1647, #1654, #1659, #1663, #1666, #1669, #1673, #1674, #1682, #1686, #1692, #1697, #1704, #1708, #1710, #1716, #1730, #1734, #1738, #1740, #1742, #1744, #1747, #1760, #1765, #1771, #1775, #1780, #1786, #1797, #1803, #1806, #1814, #1816, #1820, #1828, #1842, #1852, #1854, #1882, #1894, #1895, #1896, #1946, #1961, #2028, #2046.
  • Tests for the hosted gem, vendored Maven and pnpm revert fixes: real-Bundler and real-Maven (log4j-core) capstones, a byte-exact pnpm revert in the ledger schema e2e, and expectations reconciled across the merged fixes (#2052).

[5.0.1] — 2026-10-13

Fixed

  • 5.0.1 is the first v5 release on npm and crates.io. v5.0.0 was tagged and its standalone binaries were published on GitHub, but its npm and crates.io publishes failed on a trusted-publisher mismatch, so @socketsecurity/socket-patch and the Cargo crates stayed on 4.0.0. npm now publishes from the Release workflow its trusted publisher names. Upgrading from 4.x through npm or Cargo goes straight to 5.0.1; read the v5 migration guide and the 5.0.0 notes below first.
  • Hosted rollback / remove behind a registry mirror that passes npmjs tarball URLs through keep Yarn Berry's ::__archiveUrl= binding and pnpm's tarball: field, so the next cold install no longer 404s (#1465).
  • A Yarn Berry vendored revert rebuilds the restored yarn.lock entry from the live lock instead of the one recorded at vendor time, so yarn install --immutable keeps passing after later lock changes (#759).
  • Hosted pnpm reads npm_config_* / pnpm_config_* environment settings only where the project's pnpm major reads them, so an npm-era package-lock=false under npx no longer blocks pinning a lock pnpm 11/12 installs from (#1460).
  • A yarn classic pin written by v4 for a patch that adds a dependency the lock never locked is no longer confirmed on rescan (redirect_yarn_classic_dep_manifest_unlocked), and vex omits hosted npm-family copies whose new dependencies aren't installed (dependency_not_installed) (#1462).
  • uv: after uv remove or an upgrade of a hosted-pinned transitive package's parent, list warns hosted_resolution_orphaned and rollback / remove retire the leftover override wiring instead of failing with hosted_wiring_contested (#1445). Reverting a vendored PEP 723 script lock restores the specifier the script declares now, not the one recorded at vendor time (#869).
  • Poetry 1.3 locks are refused for a source = "pypi" dependency, like 1.4–2.2 locks, since Poetry 1.3 can't solve the rewired lock (#1447).
  • The stale-install remedy for a Pipenv --site-packages venv importing a base-interpreter copy installs over it inside the venv, instead of an uninstall pip refuses (#1450).
  • vendor --check explains a missing or changed vendored artifact in plain words, naming the file and the repair step, in human output and the --json event reason (#1467).
  • A hosted pin beside a file: copy of the same release (tarball or directory) no longer passes silently. The registry entry is still pinned, but the in-run --vex no longer attests the package not_affected, since npm and pnpm keep installing the file: copy unpatched. npm now also warns about the directory shape (#1481), and pnpm warns redirect_copy_unpatched naming the lock and entry (#935; vendored pnpm still writes its override silently beside such a copy).
  • Path-scoped scan --mode agent <member> and rollback <member> reach an npm alias dependency ("lp": "npm:left-pad@1.3.0") that pnpm's isolated linker or npm's install-strategy=linked links into the member, instead of reporting success with the copy unpatched (#1266).
  • A hosted scan / get from a workspace member reads the root's workspaces globs the way the lock's package manager does (yarn classic ignores ! negation, yarn Berry supports extglobs), so it refuses with redirect_workspace_lockfile_elsewhere instead of reporting success with nothing pinned (#1097).
  • Bundler: a Gemfile lockfile "custom.lock" call is honored, so hosted scan pins the lock bundler actually reads instead of a leftover Gemfile.lock (#1477). Hosted rollback / remove keep the DEPENDENCIES ! for a gem in the user's own source … do block after an assignment-form if / case / begin construct, so frozen installs on Bundler 4.0.19+ keep passing (#1476).
  • Hosted scans of Hatch, Poetry and PDM projects with many patched dependencies are much faster: the Hatch files and the poetry.lock / pdm.lock are rewritten in one pass instead of once per package (#1471, #1473).

Maintenance

  • Release binaries build with one codegen unit, about 16% smaller.
  • npm publishing runs inline in release.yml (publish-npm.yml is removed), and the Release workflow gains an npm-retry-version input that re-stages npm from an existing GitHub release.

[5.0.0] — 2026-10-12

v5 centers the workflow on scan (hosted patches), vex (OpenVEX attestations), and vendor (committed patched packages), with list for inspection. See the v5 migration guide before updating existing automation.

Breaking changes

  • scan and get default to hosted mode for a project that has no patches yet. scan never prompts, and hosted or vendored get selects without an interactive menu. Explicit --mode agent retains in-place patching; get --save-only and global targeting select agent behavior by default. A mode-less global scan or scan --prune does not acquire new patches, though --prune still cleans up obsolete state.
  • A bare scan / get keeps the mode a project already uses: a vendor ledger selects vendored mode and an agent manifest selects agent mode, so the quick start no longer converts an existing project to hosted in place. Switching modes (in either direction) takes an explicit --mode; a project holding both kinds of state, or vendored packages beside hosted pins, is a usage error (mode_ambiguous, exit 2) that names the packages. Human runs print a note naming the mode kept (#1088, #1437).
  • Every command's --json output is one envelope: command, status, dryRun, events, summary, warnings, plus error: {code, message} on failure and gc when a sweep ran. scan, get and rollback drop their own shapes: per-package outcomes (including apply.patches, download, the nested vendor envelope, redirect.patches, rollback's results[], rolledBack and hosted.* lists) are events, and the count keys (found, redirected, …) are replaced by summary, which always equals the event counts. Statuses and actions are camelCase only (new action rolledBack; would_* previews are verified), warnings are always top-level {code, detail}, and events from the vendored and hosted legs carry details.mode. Usage, lock and early errors print a full envelope, and scan --mode agent --json no longer prints two documents. A corrupt or unreadable manifest is manifest_invalid / manifest_unreadable on every command. See the CLI contract (#931).
  • Artifact garbage collection reports one shape on repair, remove, rollback and scan --prune: a top-level gc object (removedBlobs, removedDiffArchives, removedPackageArchives, bytesFreed) and summary.bytesFreed. repair's GC event no longer bumps summary.removed; rollback counts every leg it undid (#1257, #1066).
  • get, remove and rollback share one package-target grammar: a UUID, CVE-… / GHSA-…, a purl, or an exact package name. get <name> no longer fuzzy-matches (get yaml used to patch yaml-ast-parser) and searches every installed version of that name; with no match it reports noMatch and suggests close names. A name that reaches several packages (core) is refused with the candidates listed (ambiguous_target). remove and rollback accept a bare name or versionless purl, selecting every recorded version. Package identity is case-preserving where the ecosystem is, so remove jsonstream no longer also removes JSONStream. get honors --ecosystems and socket.yml policy on every path, including get <uuid>, and the bare-UUID shortcut works after global flags (socket-patch --json <uuid>) (#453, #1280, #1292).
  • --download-mode is removed: patch content is always fetched as per-file blobs, and the diff download path is gone (#792).
  • Hosted mode writes no ledger. list, VEX, and update discovery read the live dependency references. rollback and remove restore upstream registry entries rather than replaying saved edits, and refuse where restoration is unavailable (including offline operation and binary bun.lockb). Legacy hosted ledgers are read only for compatibility and removed by full rollback.
  • rollback restores dependencies and removes patch records and unused artifacts. --preserve-state, also available on remove, keeps local state for reuse. Hosted state has no local artifact to preserve.
  • Vendored scans and targeted gets are manifest-free. The vendor ledger embeds patch records; repair no longer reconstructs a missing ledger from lockfiles. vendor can eject hosted pins when no agent manifest exists. Reverting an ejected package restores upstream dependencies.
  • Vendored Cargo patches use workspace-root Cargo.toml wiring and tagged <version>+socket.<uuid> versions, visible to CARGO_PKG_VERSION. Re-running vendoring or repair migrates older config-file wiring and untagged copies.
  • socket.yml policy now constrains scans. Invalid patch policy fails before requests or writes. Discovered test and fixture projects are excluded by default; literal project targets skip those defaults. Hosted/vendored PATHs outside the repository are rejected.
  • Automatic patch selection prefers the highest severity, then the most advisories fixed, then publication date. Existing patches change only when the new patch outranks them; tier/UUID tie-breaking alone does not trigger replacement.
  • setup, its publishing helpers, and the PyPI/RubyGems CLI distributions are removed. Standalone binaries, Cargo, and npm remain supported; Python and Ruby project support is unchanged. Remove old hooks using the migration guide.
  • Removed scan --redirect, scan --detached, mode aliases host/redirect/ vendor, the unimplemented --one-off flags, and the three legacy SOCKET_PATCH_* environment aliases listed in the migration guide. .socket/packages/ archives are no longer consumed; cleanup removes leftovers.
  • list on an empty project exits 0; get usage errors exit 2. Human help and output are grouped by task, with diagnostic codes retained in JSON and verbose output. Hosted JSON identifies lockfiles instead of a ledger; rollback's vendored results contain vendor-owned entries only. See the CLI contract for exact schemas.
  • VEX requires live hosted/vendored wiring and reports corrupt vendor ledgers. Verified agent patches no longer require a setup hook for attestation.
  • The core crate removes setup-related modules, obsolete public helpers, and the unused DepOverride::berry_zip_url field. Patch references containing berryZipUrl still parse.
  • Removed scan --apply (use --mode agent), scan --vendor (use --mode vendored), get --no-apply (use --save-only), and the download (for get) and gc (for repair) subcommand aliases. Each is now a usage error (exit 2). --sync remains shorthand for --mode agent --prune (#966).
  • SOCKET_FORCE is no longer read. --force on apply, vendor and --update is flag-only, so a stale export no longer weakens checks in unrelated commands (#615).
  • Every --json top-level error is now an error: {code, message} object; the top-level errorCode field and string-valued error are gone. scan, get and rollback use {status: "error", error: {code, message}}, and usage errors (exit 2) print the coded error on stdout under --json (#704).
  • --cwd, --global-prefix and --manifest-path are validated before every command. A path that does not exist is a usage error (exit 2) instead of an empty project that passes silently. get and scan may still create a missing manifest directory (#1029).
  • apply --check verifies every in-scope manifest patch against the installed copies, not only Go redirects. An unpatched, tampered or incomplete copy is drift (exit 1, not_applied / hash_mismatch / file_not_found / no_matching_variant); a Yarn PnP tree is refused as apply refuses it (#1029).
  • Agent-mode apply and rollback refuse a patch that would write outside the package's install tree (OutsideInstallTree), for example through a Composer path repository symlinked into first-party source, a flit --symlink install, or a Homebrew/Nix package linked into site-packages.
  • A patch that creates a new file where other content already exists is now a content mismatch: --strict refuses it, and the default overwrites it with a content_mismatch_overwritten warning instead of overwriting silently.
  • A symlinked .socket directory is refused for vendored writes and reverts (vendor_dir_symlink_unsupported) and for the hosted/vendored ledgers. Agent-mode manifest and blob state under a linked .socket still works. Inline patch blobs are verified against their hash before they are stored.
  • The npm wrapper exits 128 + signal (130 for SIGINT, 143 for SIGTERM) when the binary is killed by a signal, instead of 1. The npm package no longer ships src/, the tsconfig or compiled tests.
  • Hosted mode now wires Gradle builds itself instead of printing a paste-able exclusiveContent snippet: scan writes .socket/gradle/ files, settings apply from lines, lock entries and verification metadata to commit. The snippet remains only as the fallback after a refusal (redirect_gradle_manual_snippet) (#646).
  • A Gradle-only build no longer has ~/.m2 scanned or patched unless the build or an init script declares mavenLocal() (or that can't be ruled out). Agent mode now patches every cache copy a JVM build consumes instead of only the first, and fails runs it cannot fully patch (gradle_ro_cache_shadows, gradle_copy_unexpected_bytes, …) (#349, #551).
  • scan --mode agent --json and get --json report a patch whose in-place apply failed as a failed event with its error code, count it in summary.failed, and count only patches that really applied as applied. They used to list it as added with failed: 0 (#424).
  • vendor --check is stricter: it fails (vendor_check_failed, exit 1) when a lockfile no longer points at a vendored entry in any ecosystem, not just Maven/Gradle, and reports lockfile references to .socket/vendor/ that no ledger entry owns as vendor_ledger_missing (#725, #831).
  • Single-module Maven projects are vendored like reactors: the dependency is pinned to <version>-socket.<hex8> and served from a committed .socket/vendor/maven2/ tree, with .mvn/maven.config and a fallback socket-patch-vendor repository, replacing the same-version <repository> wiring and .socket/vendor/maven/<uuid>/. A project vendored by an earlier release is refused with vendor_jvm_shape_unsupported (reason legacy_maven_root) and nothing is written; run vendor --revert, then vendor again. remove, rollback and hosted takeover still unwind the old wiring. VEX attests the new pin from .socket/vendor/state.json, so commit it. Without a Maven Wrapper, vendoring warns vendor_jvm_degraded; several vendor_maven_* codes and vendor_gradle_unsupported are retired (see the migration guide) (#973).
  • A token whose organization cannot be resolved (no --org, SOCKET_ORG_SLUG or socket-cli defaultOrg, and GET /v0/organizations fails) no longer queries /v0/orgs/default/… while downloads go to the public proxy. The org is resolved once per run and the whole run uses the public proxy anonymously (free patches only), warning once; --json output of scan, get, apply, vendor, vex and repair reports it as api_auth_fallback in warnings[]. An embedded --vex reuses the run's org instead of resolving it again (#648).

Added

  • Vendored Maven reactors, with committed repositories, reversible wiring, repair, rollback, and VEX. Reactors use suffixed versions; vendor --check audits artifacts and wiring offline; --local-repo checks Maven cache conflicts and --maven-config=none selects the fallback file repository. Single-module pom.xml projects are vendored the same way, as a reactor of one (#973).
  • Full Gradle support (6.8+, Groovy and Kotlin DSL; tested on 6.9 through 9.8) in every mode (#646):
    • Discovery reads Gradle's files-2.1 cache and GRADLE_RO_DEP_CACHE, finding the user home the way Gradle does; --global-prefix accepts a Gradle user home. JSON packages gain inLock, and run warnings gain a level (#349, #551).
    • Agent mode patches every hash-dir copy a build consumes, swaps the whole jar for jar-member patches (#264), keeps ~/.m2 .sha1/.md5 sidecars in step, and refuses or warns on verification metadata, read-only cache shadowing, stale transform copies and daemon-held jars.
    • Hosted mode pins the patched -socket.<hex8> version through an owned settings script that covers transitive, range, dynamic, rich, catalog, buildSrc and included-build requests, with a sha256 tripwire. Rollback and remove restore offline (#347, #348, #396, #511).
    • Vendored mode supports mixed pom.xml + Gradle roots, autocrlf checkouts, classifiers, ranges and pgp-only verification entries, and refuses subprojects (#395, #428, #429, #461, #487, #511, #533).
    • VEX re-hashes every copy a build may load and withholds on unpatched derived copies (vex_gradle_unpatched_copy).
  • sbt, Mill and scala-cli support within the maven ecosystem (#690):
    • Agent mode patches Coursier caches (sbt 1.3+, sbt 2, Mill, scala-cli) and Ivy caches (sbt 0.13–1.2), joining Gradle's every-copy patching and VEX.
    • Hosted sbt (0.13.18+) writes one generated socket-patch.sbt, gated on fresh sbt update results; vendored sbt writes socket-patch-vendor.sbt over a committed suffixed tree.
    • Vendored scala-cli directory builds get an owned socket-patch.scala and a committed Coursier tree (Linux/macOS). Mill and hosted scala-cli get paste-able snippets.
  • Hosted Yarn Berry pins dependencies declared as catalog: / catalog:<name> (#632).
  • vlt 1.3 locks with Brotli tarball nodes are accepted in hosted and vendored mode (#372).
  • Vendored Pipenv projects can move to a newer patch for an already vendored package, including from a venv installed from the older vendored wheel (#769).
  • Vendored requirements.txt supports marker-split pins (as written by uv pip compile --universal), rewriting only the matching branch (#928).
  • vendor, scan --mode vendored and get --mode vendored dry runs warn about symlinked files the real run would refuse to rewrite (#627).
  • socket.yml patch policy for paths, ecosystems, packages, severity, and per-run limits. scan --package, --min-severity, --max-new-patches, and --no-socket-yml support targeted and gradual rollout. Already-patched packages retain protection when excluded; updates do not spend the new-patch budget. Disk scans and the in-memory hosted engine share these rules.
  • Manifest-free VEX discovery from hosted and vendored references, including fresh checkouts. Product inference covers Go, Composer, Maven, NuGet, and RubyGems in addition to existing formats. Embedded VEX supports the same evidence checks.
  • vlt support across agent, hosted, and vendored workflows, with native installer coverage and explicit version/layout refusals.
  • Native binary bun.lockb reading and rewriting, alongside text bun.lock, without invoking Bun or converting binary locks to text.
  • Expanded Python lockfile support for uv, Poetry, PDM, and Pipenv, including lock-only inventory, supported multi-version/marker shapes, and stale-install diagnostics. Unsupported installer formats are refused before writes.
  • npm 12 hosted allow-remote configuration and dual-lock handling, plus pnpm trust-lockfile configuration. Explicit user settings are respected.
  • Path targeting on scan and rollback, hosted update detection from lockfiles, and configurable API request concurrency.
  • Hosted --json output reports every granted patch as an event: pinned / would-pin, skipped (with its reason) or unpinned (redirect_unconfirmed), so unwired patches are visible to tooling (#1029).
  • vlt 1.3.0–1.3.7 are supported (#1087).
  • Unknown subcommands get a precise usage error: setup and unlock name the release that removed them and the replacement, update points at --update, and hidden internal subcommands are no longer suggested (#1043).
  • Re-vendoring a uv project, a uv script lock / pylock.toml, or a Hatch project to a newer patch of the same release now works instead of failing with pypi_uv_source_already_exists / pypi_hatch_unsupported; vendor --revert still restores the original files byte for byte (#742, #650).
  • Hatch's out-of-tree environments (including Hatch 1.0–1.2 layouts) are found by agent mode, stale-install checks and VEX. Stale Hatch envs warn with the hatch env remove / prune remedy (pypi_hatch_stale_install in vendored mode) (#335).
  • Hosted re-pins to a superseding patch replace the old generation's wiring: Cargo drops the old [registries.socket-patch-*] block, Go drops stale go.sum lines (go mod tidy -diff stays clean), and Maven re-pins an earlier -socket.<hex8> literal and refreshes a rotated token URL (#864, #682, #266). remove/rollback of either generation's uuid unwind the live pin too (#999).
  • remove and rollback accept any PEP 503 spelling of a PyPI name (typing_extensions, Jinja2, ruamel.yaml) (#1024).
  • VEX product detection resolves submodules and linked worktrees to their own origin, honors GIT_CEILING_DIRECTORIES, stops at the home directory, and warns when it skips a checkout owned by another user.
  • HTTPS trusts the platform certificate store and SSL_CERT_FILE / SSL_CERT_DIR in addition to the bundled Mozilla roots, so socket-patch works behind a TLS-inspecting corporate proxy. Certificate verification is never disabled (#1107).
  • socket-patch reads Go settings saved with go env -w (GOPRIVATE, GOPROXY, GOMODCACHE, …) the way the go command does, so private modules are no longer requested from proxy.golang.org, mirrors are used and a relocated module cache is found (#344).
  • Every Go mode warns when a committed vendor/modules.txt or the consumer go.mod needs a resync to build with the patch (go_vendor_modules_txt_out_of_sync, go_requirements_out_of_sync), naming go mod vendor / go work vendor or go mod tidy; apply --check and vendor --check treat it as drift (#343, #618).
  • Vendored Pipenv and uv projects also wire an exported requirements.txt (pipenv requirements, uv export) and Pipenv's pylock.toml (use_pylock) to the same committed wheel, so installs from those files get the patch and vendor --check / vex stop reporting contested wiring (#612, #1368).
  • --update and the update notice give pnpm installs pnpm commands (pnpm add -g …, pnpm add …, or pnpm update --recursive --latest … at a workspace root) instead of an npm command that does nothing or writes a stray package-lock.json (#1111).
  • scan and apply warn gem_bundler_plugin_stale when Bundler still registers the v4 socket-patch plugin at a deleted path, naming bundle plugin uninstall socket-patch (#1295).
  • Vendored yarn classic workspaces warn yarn_classic_workspace_member_install_risk: a cold-cache install from a member directory can't fetch the vendored tarball, so install from the workspace root or use hosted mode (#691).
  • Telemetry records hosted-mode outcomes (patch_applied / patch_apply_failed with mode: "hosted"), self-updates (cli_updated / cli_update_failed), the requested and already-wired modes on patch_scanned, and the ecosystems the crawl actually found. CLI_CONTRACT.md lists which command sends which event.
  • docs/ecosystems.md defines v5 support tiers (Supported, Beta, Legacy, Not supported) per ecosystem and mode. Hosted and vendored Maven/Gradle/sbt are Beta; binary bun.lockb, vendored pnpm 7/8 locks and early vlt lock eras are Legacy (#1156).

See ecosystem support and the compatibility guides for format boundaries, integrity limits, and required install commands.

Fixed

  • Credentials no longer leak to logs, JSON, telemetry, VEX or the patch server. Hosted Composer pins drop the lock entry's transport-options (and source), so composer install no longer sends a private registry's auth headers, client certificates or proxy settings to the patch server (redirect_composer_transport_options_removed) (#399). URL userinfo, grant tokens and secret query values are redacted from warnings, --json events, error messages, debug output and telemetry; patch_fetch_failed sends a uuid only when the target was a UUID; and the VEX product @id never carries credentials from a git remote. Subcommand --help no longer prints the value of SOCKET_API_TOKEN / SOCKET_CLI_API_TOKEN.

  • --manifest-path selects one project for every command. rollback, remove, repair, apply --check, vex and agent-mode scan / get used to read (and rollback / remove to write) the vendor ledger in --cwd instead, so one run could unwind two projects (#745).

  • repair and scan --prune keep the original-file blobs of patches still in the manifest, so an offline rollback after a repair works again (#893).

  • Human scan --mode vendored --prune runs the vendored GC when packages are found but none is patched, as --json already did (#1127). Human and --json scan exit 0 alike when the patch API answers with no records, and partial detail-query failures warn in human output too (#1062).

  • The apply --check drift remedy repeats the check's own --cwd, --manifest-path, -g / --global-prefix and --ecosystems flags, instead of a bare socket-patch apply that acts on another tree (#1219).

  • Hosted rollback / remove, takeover and eject look up upstream registry entries a few at a time instead of all at once, so many pins no longer trip registry rate limits or EMFILE (#1220).

  • The OpenVEX document is written with stage + rename, so a failed write no longer leaves a truncated document behind (#1144).

  • Vendored Maven, Gradle and NuGet artifacts are protected from .gitignore rules such as *.jar / *.nupkg, and vendoring is refused when git would still drop them, as npm-family vendoring already was (#1061).

  • scan --vex in hosted mode no longer attests an npm patch as not_affected when package-lock.json also lists a bundled copy of the same name@version (inBundle, or bundled in a v1 lock). npm unpacks that copy from its parent's tarball, so it stays unpatched; the run already warned redirect_npm_bundled_instance_skipped and now leaves the patch out of its attestation, like a standalone vex run (#325). When a packages map exists, stale bundled flags in the legacy dependencies mirror do not suppress an attestation for the actual install tree.

  • vex no longer attests an npm or Bun patch as not_affected while a second entry for the same name@version in the same lockfile still resolves from the registry (for example a workspace member added after vendoring). That copy installs unpatched, so the patch is now reported as contested. vendor --check reports the same lockfile entry as drift (#588).

  • Global mode (-g) finds npm, yarn, pnpm, bun, RubyGems and Composer on Windows, where they install as .cmd / .bat shims, instead of reporting an empty scan. The yarn and npm-family global lookups no longer run from the scanned project, so a Yarn Berry project's global script can't run or pick the directory treated as the global install. Composer's global home also falls back to %APPDATA%\Composer and $XDG_CONFIG_HOME/composer.

  • Agent-mode PyPI apply patches every installed copy of a release, not just the first one found. A Pipenv project with both a WORKON_HOME venv and a ./.venv, or a global install with the same release in the user site and a system dir, no longer keeps the copy Python imports unpatched while vex attests it (#529, #501).

  • Gem hosted and vendored modes wire only the manifest Bundler loads. A gems.rb twin or a BUNDLE_GEMFILE setting (environment or .bundle/config) no longer leads to an edit of an ignored Gemfile that reports success and attests an unpatched gem; unsupported layouts are refused before any write (#341, #390).

  • Gem modes read Bundler settings in Bundler's own priority. A BUNDLE_GEMFILE in .bundle/config now outranks the environment variable, so a dual-boot project with an exported BUNDLE_GEMFILE=Gemfile is no longer wired through the Gemfile Bundler ignores (#507). The hosted stale-install guard checks the committed archive in Bundler's configured cache dir (cache_path / BUNDLE_CACHE_PATH) instead of always vendor/cache, so a stale archive there now warns and keeps the same run's VEX from attesting it (#483). Both settings skip .bundle/config under BUNDLE_IGNORE_CONFIG, as Bundler does.

  • npm dependencies installed from git, a URL or file: are no longer reported patched. npm installs such a dependency from the dependent's spec (github:user/repo, https://…/x.tgz, file:…) and ignores the lock entry's resolved, so npm ci kept installing the original bytes after scan --mode hosted or vendor rewired the entry and vex attested it. Both modes now skip such an entry with a loud stays-UNPATCHED warning (redirect_npm_non_registry_entry_skipped / vendor_non_registry_entry_skipped; vendoring refuses with vendor_lock_entry_not_rewritable when no registry copy is left), and vex attests nothing for a name@version while such a copy is in the lock (#326). A dependency the project's overrides send back to a registry version is not one of these: npm installs the override's registry release, so hosted and vendored modes patch it again, and vex attests it (#490).

  • Agent mode finds Poetry's virtualenv in more setups. Three cases missed the virtualenv Poetry installed into. Each fell back to the wrong interpreter, skipped the patch as package_not_installed and still exited 0:

    • a nameless package-mode = false project (Poetry names its env non-package-mode-…);
    • a Poetry 2 project with both [project] name and [tool.poetry] name (Poetry uses [project] name);
    • an explicit virtualenvs.in-project = false next to a stray ./.venv.

    Every Windows project missed it too, because the cwd hash included the \\?\ prefix that path canonicalization adds (#327, #329).

  • Hosted Maven warns redirect_maven_trusted_checksums_unenforced when .mvn/wrapper/maven-wrapper.properties pins a Maven older than 3.9.4. Maven 3.9.0–3.9.3 never enforce the Trusted Checksums pin that hosted mode writes; the 4.0.0 notes and docs wrongly said every 3.9 release does. The version suffix still fails closed. CI runs the real-Maven hosted capstone on 3.9.3 and 3.9.4 (#258).

  • Patch application, reversal, and cleanup handle missing files, release variants, corrupt state, newer ledger formats, and unsafe manifest paths without silently dropping protection. File ownership restoration failures produce warnings.

  • Hosted Cargo handles v1 locks, CRLF files, and repeated declarations, and refuses transitive dependencies its registry pin cannot reach. Vendored Cargo preserves multiple versions and warns about old-toolchain limitations.

  • Go preserves user-authored replacements, handles +incompatible versions, restores checksums, and unwinds hosted references during vendoring. Registry fetches honor GOPROXY and private-module settings.

  • Yarn Berry preserves supported line endings and checksum spellings. Mode preflights, including Bun's, run before discarding existing protection.

  • Yarn Berry hosted references no longer send npm registry credentials to the patch server. The old npm: locator made yarn attach npmAuthToken / YARN_NPM_AUTH_TOKEN to scoped packages (and to every package under npmAlwaysAuth). Hosted mode now pins the way yarn does for a root resolutions entry: package.json routes the locked descriptor to the hosted tarball and the lock entry is keyed by it, which also passes yarn's hardened mode (on by default for public pull request CI). A user-authored resolutions entry for the package is never overwritten. Locks pinned by earlier releases are re-pinned on the next hosted scan.

  • Composer hosted references remove upstream source fallbacks and mirrors; RubyGems hosted locks preserve source order; NuGet edits use the active config and survive <clear /> entries.

  • Python rewrites preserve supported markers, groups, extras, source metadata, and integrity pins. Relocks, out-of-tree environments, and lock-only VEX are handled consistently with each installer's supported behavior.

  • Hosted Pipenv scans read the Pipfile, so a conflicting Pipfile.lock entry refuses the patch project-wide instead of half-redirecting a sibling requirements.txt (#333).

  • Vendoring reuses valid committed artifacts during service outages. Updates do not build from a previous patch's modified bytes. Verified service artifacts keep their identity; integrity failures do not fall through to a local rebuild. Repair rebuilds against recorded pins and reports unavailable inputs.

  • VEX no longer attests an npm package that also ships a bundled, unpatched copy of the same name@version (inBundle: true, or v1 bundled: true). npm unpacks that copy from the parent's tarball, so no rewire reaches it; the reference is now reported patched_ref_unattributable, naming the bundled copy, in hosted and vendored mode (#325).

  • API throttling uses bounded retries, failed queries appear in JSON diagnostics, and hosted reference resolution handles batches larger than 500 patches.

  • Transient apply locks are removed on normal command exit; no-op scans and full reversal avoid leaving unused .socket/ state. Terminal output, telemetry timeouts, and update-check handling are more consistent.

  • Agent mode finds transitive npm packages in npm's linked store (install-strategy=linked, node_modules/.store) and in a relocated pnpm virtualStoreDir, instead of reporting them package_not_installed (#359, #362). A store outside the project, such as pnpm's global virtual store, is shared with other projects and is still not patched in place.

  • Agent mode and vex find packages installed under pnpm's modulesDir (modulesDir: in pnpm-workspace.yaml, or modules-dir in .npmrc). From pnpm 10.12 the virtual store moves there (<modulesDir>/.pnpm), so apply exited 0 with the package unpatched as "not installed", and hosted vex attested not_affected over the unpatched install. Hosted vex also no longer attests a pinned npm package the crawler cannot see because pnpm keeps the installed store outside the project (#661, #696).

  • npm locks keep their own layout when edited. scan --mode hosted, scan --mode vendored, rollback and vendor --revert re-serialized package-lock.json / npm-shrinkwrap.json with LF line endings (and, in hosted mode, a fixed 2-space indent), so a CRLF or tab-indented lock got a whole-file diff and the undo did not restore its bytes. A lock with a UTF-8 BOM, which npm installs from, was skipped as unparseable (hosted) or refused as vendor_lockfile_version_unsupported (vendored). The lock now keeps its BOM, indent and line endings, and the undo is byte-exact (#324).

  • Agent mode no longer patches other projects through a store they share. PDM 2.0–2.12 with install.cache and cache_method = symlink links site-packages/<pkg> into its package cache, and pnpm's global virtual store (enableGlobalVirtualStore) links node_modules/<dep> into <store>/links. apply (also -g) wrote the patch into that shared directory, so every project using it was patched, and a rollback in one project silently unpatched the rest. apply and rollback now fail on such a package, naming the store and how to get a private copy (#332, #361).

  • vendor under --global / --global-prefix (or SOCKET_GLOBAL / SOCKET_GLOBAL_PREFIX) is now a usage error (exit 2, global_scope_unsupported), like scan and get with --mode vendored. Run inside a project, vendor -g vendored the manifest's records into that project and rewired its lockfile, and vendor --revert -g unwound the project's vendoring, so its next frozen install was silently unpatched. Global installs have no project lockfile to vendor into (#498).

  • Patch API requests (scan, get, apply and vex lookups, and blob and diff downloads) no longer hang forever on a stalled proxy, load balancer or half-open connection. A connect now fails after 10 s, and a connection that sends nothing for 60 s fails as a network error. Downloads that keep streaming are not cut off (#570).

  • Patch blob and diff downloads stream straight to the .socket cache instead of being held in memory whole first, so a large patch artifact no longer costs its full size in RAM during apply, get, repair or rollback (#571).

  • Hosted scans no longer write a pin that vex, list, rollback, remove and vendor would then refuse as contested (for example a Pipfile.lock beside a requirements -r include that still pins the registry, or a Maven pin inside a <profile>). Such a patch is skipped as redirect_unattributable and nothing is written for it (#567, #260).

  • A stale hosted URL in an unrelated field, inactive lock or comment no longer counts a patch as already pinned, so it cannot slip past --max-new-patches. Lockless NuGet/Cargo pins warn redirect_pin_lockless and name the lockfile to create (#1058).

  • Taking a vendored package over to hosted mode is atomic: if the hosted pin cannot be written, the package stays vendored byte for byte (redirect_takeover_kept_vendored) instead of being left unpatched in both modes, and the dry run now reports the real outcome (#1039).

  • scan --prune can reclaim unused vendored entries for every ecosystem (previously only npm, Cargo and requirements.txt), and scan reports them as vendor_ledger_entry_unwired instead of resurrecting them. Entries the package manager still installs, or whose wiring cannot be read (including Gradle), are kept (#1050).

  • Vendored Maven and NuGet no longer treat a commented-out (or profile-scoped) repository or package source as existing wiring. vendor --check reports orphaned JVM trees whenever the ledger has no JVM entry, not only when it is empty (#1050).

  • Purl matching folds NuGet case, PEP 503 names and Composer version padding everywhere, so scan --prune no longer drops a live NuGet entry whose API and installed spellings differ, and remove/rollback accept either spelling. Policy and rollout reports show the folded spelling (#1045). NuGet versions are also normalized (1.0.0.0 matches 1.0.0), so a package vendored under a 4-part packages.config version is no longer treated as unused by VEX and vendor --check, or reverted by scan --prune (#1202).

  • Yarn classic file:, URL and hosted-git copies are no longer repointed at Socket's registry artifact (hosted or vendored); they are skipped with a stays-unpatched warning, and rollback refuses such a pin from an older release. A block with no resolved line is reported instead of counted as patched; mixed CRLF/LF yarn.lock files keep each line's ending, and a $ in a patch-server URL no longer corrupts the rewrite (#1057).

  • sbt builds marked only by project/build.properties and scala-cli builds with only .scala-build/ are discovered. --ecosystems maven now includes vendored JVM ledger entries in rollback, repair and GC, and JVM vendoring no longer requests server builds it will not fetch (#1032).

  • Vendored Gradle reads verification-metadata.xml like Gradle: markup in CDATA is text, and an unterminated section is refused as gradle_verification_unparseable instead of being edited (#715).

  • Gem hosted and vendored modes refuse a custom BUNDLE_LOCKFILE they would not pin, wire a Bundler 1.x Gemfile + gems.rb twin through the Gemfile, and refuse twins whose locks disagree on the Bundler major. A lock-only scan reports gem_lock_unsupported instead of finding nothing (#749, #751).

  • npm workspace members holding a stray package-lock.json are refused in hosted and vendored mode, naming the workspace root, instead of pinning a lock npm never reads (#1094).

  • Rollback and remove of an agent patch superseded by a hosted pin restore orphaned store copies (Bun isolated linker) that still hold the patch, and fail without dropping the record if one cannot be restored (#1084).

  • apply.lock is removed when a run is interrupted with Ctrl-C, SIGTERM or SIGHUP (and Ctrl-Break/console close on Windows) (#808).

  • Vendored PyPI revert keeps the wheel and ledger entry while any other project file still installs it (a uv export, a pylock.toml, a -r include, a sibling or subdirectory requirements file, including UTF-16 and non-UTF-8 files), warning vendor_revert_residual_reference (#996, #867, #1167).

  • Vendored revert retires an entry whose dependency was removed by pipenv uninstall, uv remove or bun remove (bun.lockb) instead of reporting drift forever (#1132, #1140, #1142).

  • Re-vendoring Poetry and PDM projects to a superseding patch works instead of failing with pypi_poetry_source_already_exists (#1136).

  • A requirements.txt and its -r includes are treated as one install set, so a duplicate pin in an include no longer contests hosted or vendored wiring (#1086).

  • Python lock inventory and repair treat cp311-none-any-style wheels as platform-locked, consistently with hosted and vendored mode, and repair pairs each wheel URL with its own hash (#1150, #1079).

  • Vendored Poetry 2.x locks are written one file per line, as Poetry does, and locks spelled name="pkg" or without a files array now wire (#936).

  • Vendored crates whose Cargo.toml has a BOM, a legacy [project] table or dotted keys are found (#693).

  • Manifests with uppercase beforeHash/afterHash no longer fail apply and rollback verification (#707).

  • Lines inserted into files with mixed line endings use the majority ending, so one stray CRLF no longer turns every new line CRLF (#815).

  • Remedies no longer suggest a per-package vendor --revert, vendored symlink refusals share hosted mode's redirect_symlinked_file_unsupported code, and a refused hosted remove prints its error once (#1043).

  • VEX no longer attests a package beside an unpatched copy in the same lock or loader: a pnpm file: copy or bundledDependencies copy, a yarn classic registry block beside the Socket block, a yarn berry file:/url copy or registry locator of the same version, a stale yarn PnP loader that still resolves the registry copy, and a deno.lock entry that Deno installs instead of package-lock.json (#935, #938, #939, #519, #406). Bundled and deno.lock cases are reported as vex_pnpm_bundled_copy / vex_deno_lock_copy and do not fail vendor --check.

  • VEX strips credentials, query and fragment from a non-GitHub/GitLab/Bitbucket git remote used as the product id, so CI tokens no longer leak into the OpenVEX document. A FIFO at --output no longer hangs a failed vex run.

  • Mode switches no longer strand a package unpatched. When a takeover is refused, the previous wiring is kept byte-for-byte and --dry-run previews the refusal:

    • hosted to vendored, for any backend refusal after the upstream restore (pnpm catalog, CRLF lock, uv inline sources, split requirements pins, a gem declared in a group block, lock-only yarn PnP) (#853, #944, #775);
    • vendored to hosted, for a uv lock at another version (redirect_uv_takeover_version_unreachable), a Poetry 0.x lock, a platform-only wheel, or a UTF-16 requirements.txt (#723, #945, #721).
  • rollback and remove of an agent record superseded by a hosted pin of a newer patch drop the record (rollback_record_superseded) and restore the lock instead of failing with "modified after patching" (#933).

  • Hosted scan/get run from an npm, yarn, Bun or vlt workspace member refuse with redirect_workspace_lockfile_elsewhere, naming the root, instead of pinning nothing and exiting 0. Brace sets, character classes and nested workspaces are matched (#884, #1071, #942).

  • vendor --check names the real cause of an unwired entry: a contesting lock to delete, or a removed dependency to clean up with scan --mode vendored --prune (#900).

  • Every file a NuGet, Maven, Hatch or pnpm vendored run writes is scanned for references, so the orphan sweep no longer deletes a unit the project still uses when its ledger entry is missing, and repair reports it (#832, #958).

  • npm and yarn:

    • Yarn PnP is decided from the effective nodeLinker (env, project, ancestor and home rc, YARN_RC_FILENAME), so a stale .pnp.js after switching to node-modules is ignored and a lock-only PnP berry project is refused before vendoring (#975, #539).
    • Hosted yarn classic resolves yarn-offline-mirror the way yarn 1.22 does (parent, user and global rc files, YARN_*/npm_config_* env, BOM files) and refuses to pin under a mirror (#1013, #1078). It warns redirect_yarn_classic_berry_migration_risk when a yarn 2+ install would drop hosted pins, unless packageManager pins yarn 1 (#907).
    • Hosted yarn berry refuses a root package.json with mixed line endings, as vendored mode does (#628, #629).
    • Agent mode patches npm linked-store alias copies ("lp": "npm:left-pad@…") and VEX checks them (#852).
    • The npm wrapper runs the musl binary on musl hosts (yarn classic on Alpine) and prints the spawn error instead of exiting silently (#974).
    • Hosted npm warns when npm would not install the pin as written: redirect_npm_replace_registry_host when replace-registry-host rewrites the hosted URL so installs fail E404 (#812), and redirect_npm_shrinkwrap_only when npm-shrinkwrap.json is the only lock, which npm 12 ignores; VEX then omits the patch (vex_npm_shrinkwrap_only) (#899). Vendored mode warns vendor_npm_allow_file when npm's allow-file setting would refuse the vendored file: tarball, and vendor --check fails it (#969).
    • Packages carrying the project's own npm 12 npm patch (patchedDependencies, or a patched lock record) stay on the registry with redirect_npm_patched_dependency_skipped instead of being pinned into an install that fails EPATCHFAILED; an entry an earlier release already pinned names the rollback that restores it. The vendored lockfileVersion 4 refusal names npm patch and the real remedies (#711).
    • Lock entries beneath a dependency that ships its own npm-shrinkwrap.json (hasShrinkwrap), which npm installs from that shrinkwrap, are skipped with a warning in hosted and vendored mode and never attested; vendor --check names the shrinkwrapping dependency (#753).
    • Vendored npm no longer refuses a registry package because a file: dependency or workspace member has the same name@version (#688), no longer fails vex and vendor --check after npm 7–12 drops resolved from a v2 lock's dependencies mirror (#879), and keeps the vendored patch with a vendor_prebuilt_pending / vendor_prebuilt_unavailable skip instead of failing the run while a superseding patch's artifact is not yet built (#954).
    • Agent scans apply socket.yml path filters and the built-in test/fixture exclusions to nested npm projects; a package shared with a skipped project warns policy_shared_copy (#554).
  • pnpm:

    • BOM-prefixed pnpm-lock.yaml and pnpm-workspace.yaml are read correctly, so hosted mode no longer skips trustLockfile or duplicates keys (#903, #904, #905).
    • Vendoring a scoped package into a pnpm 7/8 lock quotes its name:, and a re-vendor fixes locks written by earlier releases (#956). Quoted scoped aliases are refused like unscoped ones (#957).
    • Hosted scans pin the per-member locks of a sharedWorkspaceLockfile: false workspace from the workspace root instead of reporting success with nothing pinned, and list, vex, rollback and remove see those pins; an unlistable member set is refused with redirect_pnpm_member_locks_unresolved (#492). With gitBranchLockfile on and a pnpm-lock.<branch>.yaml present, hosted and vendored modes refuse (redirect_pnpm_git_branch_lockfile / vendor_pnpm_git_branch_lockfile) instead of pinning the stale pnpm-lock.yaml (#556).
    • A project under an ancestor pnpm-workspace.yaml whose packages: globs do not list it (dot directories included) is patched standalone again instead of being refused as a member (#1006). On pnpm 9.0–10.4, where it would break pnpm add, the root-only pnpm-workspace.yaml scaffold is no longer created (#734).
    • Rush: hosted runs on pnpm 11+ give a Rush-specific trust remedy (pnpm_config_trust_lockfile=true rush install, usePnpmFrozenLockfileForRushInstall), re-issued on a re-run, and warn redirect_rush_repo_state_stale for subspace repo-state.json files (#713, #714).
    • Hosted rollback and remove restore each entry from the registry the project resolves it against (.npmrc registry / @scope:registry and pnpm-workspace.yaml registry / registries, per pnpm major), and write tarball: fields only when the installed pnpm would, warning upstream_pnpm_tarball_setting_guessed when that is unknown (#919, #902).
    • Vendoring accepts a user's exact-version pin in pnpm-workspace.yaml overrides: (quoted or commented too), edits only the project document of a pnpm 11+ two-document lock (others are refused with vendor_pnpm_lock_multi_document), and reverting a vendored package no longer clobbers a vendored dependency's ref inside it (#854, #466, #830). scan/get --mode vendored --dry-run preview a refused hosted-to-vendored takeover as would_refuse (#853).
    • Agent mode: scan --mode agent <member-path> finds the member's linked copies (#778), apply and rollback no longer report a member-linked package twice (#633), and global scans of pnpm 11+ patch every pnpm add -g install's copy, not just one (#435).
  • PyPI:

    • requirements.txt includes are followed the way pip reads them: -r after other options, quoted paths, ${VAR} expansion and UTF-16/BOM files (#1028, #994, #721). A non-UTF-8 candidate file refuses hosted runs with candidate_file_unreadable instead of reporting success.
    • requirements.txt files are decoded the way pip decodes them (BOM, then a PEP 263 coding line, then UTF-8). A Latin-1/cp1252 file is no longer skipped as absent by lock-only scans, and a UTF-16 export beside uv.lock is seen by vendored routing, vendor --check and vex instead of being attested over (#1119, #1120).
    • A plain-ASCII requirements.txt whose coding line names any ASCII-compatible codec Python knows (iso-8859-15, cp1250, mac-roman, gbk, …) is read instead of treated as unreadable, so scans no longer find nothing and vex and rollback see its hosted pins (#1212).
    • Pipenv projects with use_pylock = true are wired through Pipfile.lock, which Pipenv installs from, instead of pylock.toml, and a pylock-only Pipenv checkout is refused (redirect_pipenv_pylock_unsupported / pypi_pipenv_pylock_unsupported) and not attested, instead of reporting success while pipenv sync installs the original release (#912, #1122).
    • Hosted mode withholds platform-, ABI- and interpreter-bound wheels (cp311-none-any, manylinux) from cross-platform locks with redirect_pypi_platform_wheel; vendored mode gives vendor_platform_locked (#701, #932, #1048).
    • A fresh uv checkout with no env, or PEP 723 script locks only, no longer scans the system Python (#964). vendor --dry-run previews the uv inline-table refusal (#979).
    • Vendored mode warns pypi_multiple_lockfiles when a root requirements.txt beside the governing lock stays unpatched (#612).
  • RubyGems:

    • Hosted gem no longer pins a version the project's Gemfile.lock does not resolve (redirect_gem_version_not_locked) (#1055).
    • The stale-install guard judges only the gem homes Bundler uses, removing false warnings that broke scan --mode hosted --vex on fresh checkouts and naming project-local vendor/bundle correctly (#1001, #729).
    • Agent mode patches the gems Bundler loads under path.system and in the <root>/.bundle install root (#915, #967).
    • Trailing # comments in .bundle/config values are stripped as Bundler 2.5.6+ does (#951).
    • A hosted re-scan that moves a patched gem to a new URL keeps the lock's GEM sections in Bundler's sorted order, so frozen installs on Bundler 4.0.19+ no longer fail with "Your lockfile needs to be updated"; locks left out of order by earlier runs are healed on the next scan (#1186).
  • Project-mode crawls of a Cargo project with a Cargo.lock, a Go module with a go.sum (and no go.work in effect), a restored .NET project (obj/project.assets.json) or a Deno project with a deno.lock look up only the packages that project resolves instead of walking the whole shared $CARGO_HOME registry, GOMODCACHE, ~/.nuget/packages or JSR cache, so scan and agent mode no longer patch, and VEX no longer attests, packages other projects downloaded. Projects without a readable lock or restore, Go workspaces, vendor/ trees, solution roots and global mode keep the full walk (#1204, #1207, #427, #1216).

  • scan --mode agent --json and get --json report files whose contents matched neither patch hash and were overwritten, as content_mismatch_overwritten entries in warnings[], as apply --json already did (#1004).

  • remove --preserve-state on a hosted patch prints the "no preservable local state" note and carries hosted_state_not_preservable in --json warnings[], like rollback --preserve-state (#433).

  • Registry downloads for hosted restore and vendored Maven use the 10 s connect / 60 s idle timeouts instead of a 60 s total deadline, so slow large downloads no longer fail (#872). Vendor-service retries honor HTTP-date Retry-After (#677).

  • Home-relative probes (Cargo, Maven, NuGet, Python, Ruby, Deno) no longer resolve against the working directory when HOME is unset.

  • JVM.

    • Gradle rollback refuses a before-blob that does not hash to its cache directory, and dry runs predict it; a crafted jar header can no longer exhaust memory; Gradle cache and build files are read without hanging on a FIFO (#646).
    • Hosted Maven edits only the markup Maven builds the project from: it skips comments, <profiles> and plugin dependencies, honors <classifier> and self-closed sections, and never writes a second top-level section Maven refuses (#259, #262, #342).
    • Hosted Maven refuses a multi-module reactor root (redirect_maven_multimodule_unsupported) instead of pinning only the root while a module's own <version> keeps the unpatched jar; vex no longer attests such a pin (#261).
    • Project-mode Maven scans look up only the coordinates the project's poms reach, instead of everything other builds left in ~/.m2 (#265).
    • Vendored Maven leaves a root unpinned, with a warning, when an imported BOM or an external parent manages a different (or unknown) version, so vendoring no longer silently downgrades or freezes it (#488).
    • scala-cli fails closed on an unreadable or truncated Bloop project file, as sbt does, instead of judging the remaining projects alone (#1270).
  • NuGet.

    • Hosted unwinds restore the lock's real contentHash, not nuget.org's catalog packageHash, so dotnet restore no longer fails NU1403 after rollback / remove (#624).
    • Hosted and vendored modes warn when the global packages folder already holds the upstream bytes of the patched version (redirect_nuget_stale_global_package / vendor_nuget_stale_global_package), and the same run's VEX no longer attests it (#352).
    • The written packageSourceMapping keeps feeds defined in parent and user configs working and routes the patched id exclusively to the Socket source (#354, #462).
    • Every lock the root nuget.config governs is pinned: member projects' locks, packages.<Project>.lock.json and a literal NuGetLockFilePath (#353, #514). Hosted lock reads accept a UTF-8 BOM and leave other resolved versions of the id alone in multi-targeting locks (#593, #623).
    • Vendored revert works on core.autocrlf checkouts and never leaves the config and lock half-reverted (#537); </packageSources > and other valid spellings are recognized (#685).
  • Go.

    • A replace for a version the build list doesn't select is no longer written as if it worked: apply refuses, vendor refuses (vendor_go_version_not_selected), apply --check reports a mismatch and vex omits the patch (go_version_not_selected) (#391, #392, #509).
    • In a Go workspace, apply and vendor no longer write a go.mod replace that the go.work in effect overrides, ignores or rejects; --check fails on such a replace and vex no longer attests it (#393, #458, #531).
    • Agent apply and vendor work on Windows with Go's read-only module cache instead of failing with Access is denied (#346).
    • vex attests an applied agent-mode patch for a +incompatible module instead of reporting not_applied (#484).
  • Composer.

    • Reinstall hints name the configured vendor directory (COMPOSER_VENDOR_DIR, config.vendor-dir) instead of vendor/ (#658).
    • scan -g and agent-mode scans find installs whose vendor-dir is set in the global or another inherited config, not only the project's composer.json (#439, #686).
    • Global discovery finds a Composer 2 home in the XDG config dir even when an old ~/.composer directory remains (#586).
  • npm family.

    • npm's ${VAR} expansion is applied to replace-registry-host, so a setting that expands to always gets the hosted-pin warning (#1233).
    • vendor --check fails, and repair reports vendor_dep_manifest_unlocked, for a yarn classic lock an older release vendored with dangling descriptors (#1379).
    • Hosted yarn classic pins always carry the #<sha1> fragment yarn keys its cache on, so a warm cache can't serve unpatched bytes; a tarball that can't be verified is skipped (npm_tarball_unavailable) (#558). Lock blocks for empty-range dependencies ("left-pad": "") are found (#1271).
    • A yarn classic package also installed through an npm: alias is no longer attested by scan --vex, and a takeover that would unwire a vendored alias block is retracted (#1081, #1158).
    • Yarn Berry: a re-scan pins a range added after the hosted pin, and unwinds no longer leave two entries with one resolution (#1082); restores read the registry from npmScopes, YARN_NPM_REGISTRY_SERVER and parent/home .yarnrc.yml like yarn (#1017) and put back the registry's bin: map (#1131); pins of native addons drop the registry-only node-gyp dependency (#737); and a resolutions selector left by yarn remove is reported hosted_resolution_orphaned and removed by rollback / remove instead of failing them (#1203).
    • pnpm: a keyless pnpm-workspace.yaml gets a packages: ['.'] scaffold (removed again on unwind) so pnpm 8–10.4 keep working (#1096); vendored scans skip orphaned .pnpm store entries (#1197); and a custom modulesDir with node-linker=pnp is no longer mistaken for Yarn PnP (#1129).
    • Bun: hosted unwinds read the registry from the user's .npmrc and global bunfig, as Bun does, instead of querying the public registry for private scopes (#1276); a bun.lockb pin shared with a bundled copy can be listed and unwound (#1243).
    • Upgrading or downgrading a vendored npm or Bun package reverts cleanly instead of being kept as drift (#1155).
    • Vendored bun.lockb workspaces check that git would commit the member-relative tarball mirrors, as the root artifact already was, so a *.tgz ignore rule can't break fresh frozen installs (#1116).
    • Hosted mode leaves pnpm locks alone when pnpm's lockfile setting is off (lockfile=false, lockfile: false or package-lock=false), since pnpm then installs from the registry; it warns redirect_pnpm_lockfile_disabled and suggests vendored mode, and lock-only vex no longer attests those pins (#1074).
    • Yarn Berry rollback / remove after a yarn remove in one workspace member restore the registry entry keyed only by the descriptors still declared, so yarn install --immutable passes (#1435).
    • .yarnrc.yml settings are read the way yarn reads them, including ${VAR:-default} and quoted YAML values, so a PnP project configured through a variable is refused as PnP (#1100).
    • Appending allow-remote=all to a CRLF .npmrc with no final newline keeps its line endings intact (#1196).
    • Hosted scans pin npm aliases in a v2 lock's npm 6 dependencies mirror and in v1 locks, vendoring rewires them, and VEX withholds while the mirror still resolves from the registry (#432).
    • vex no longer attests a patch wired in only one of npm-shrinkwrap.json / package-lock.json when the other lacks that name@version (#798).
    • Hosted and vendored modes no longer drop a project's own bun patch; such packages stay on the registry with a warning (hosted) or are refused (vendored) (#367).
    • Hosted Yarn classic with yarn-offline-mirror is refused with redirect_yarn_classic_offline_mirror instead of reporting unpatched installs as patched; a vendored package is kept vendored (#364).
    • Yarn classic file: directory copies are named in hosted runs and never attested; vendoring a package with only git/link/file: copies is refused as vendor_lock_entry_not_rewritable (#921, #857).
    • Hosted rollback/remove of Yarn Berry and vlt pins restore from the project's own registry (npmRegistryServer, the vlt node's registry), falling back to the default with upstream_registry_fallback (#908, #521). The warning no longer includes user:token@ credentials from the registry URL.
    • Vendored npm-family tarballs are protected from .gitignore rules such as *.tgz; a vendor dir git would still ignore is refused up front with vendor_artifact_gitignored (#831).
    • Hosted runs from a pnpm workspace member whose lock lives elsewhere (including a root lockfileDir) are refused with redirect_pnpm_lockfile_elsewhere instead of pinning nothing (#590).
    • pnpm members with their own lock no longer get settings written to an ignored nested pnpm-workspace.yaml; hosted refuses with redirect_pnpm_settings_elsewhere until the root trusts the lock, and vendored refuses with vendor_pnpm_settings_elsewhere (#880, #881).
    • Agent mode finds transitive dependencies in pnpm's global virtual store (also from workspace members) and refuses them as shared instead of reporting success (#362).
    • Apply and rollback report writes to every pnpm/vlt peer-variant store copy, not just the first (#756, #772).
  • Cargo.

    • Hosted runs from a Cargo workspace member are refused (naming the root) instead of rewriting the member as a lockless project (#417).
    • Agent mode patches, checks, attests and restores every crates.io copy in a shared CARGO_HOME (one per registry index dir, as different cargo versions use), not just the first (#339).
    • After an in-place apply or rollback, the next cargo build recompiles the changed crate instead of linking the rlib it cached before (#387).
    • apply on a cold or pruned registry cache exits 1 again and names cargo fetch, instead of reporting success while the next build compiles the unpatched crate (#616).
    • An agent-to-vendored takeover followed by rollback / remove, and scan --prune after a lock bump, no longer drop the record of a crate still patched in the shared cache (#336, #1278).
    • Hosted mode refuses a crate the user overrides with [patch] (redirect_cargo_dep_overridden) instead of replacing the override (#480). vex withholds, and restore handles, a hosted crate when a later dependency locks its own crates.io copy of the same version (#679, #863).
    • Cargo source replacement ([source.crates-io] replace-with to a directory source) is followed through every config cargo reads, so agent mode patches, and vex / rollback judge, the copy cargo builds from instead of assuming <cwd>/vendor. A vendor/ that no replace-with wires is no longer crawled. Hosted mode skips every crate when crates.io is replaced by an on-disk source (redirect_cargo_directory_source) (#338, #455).
    • Hosted mode pins a dependency declared with an explicit registry = "crates-io" instead of skipping it (#386).
  • RubyGems.

    • Gemfile lines joining declarations with ; are refused instead of losing the second gem; a declaration ending in a bare ; is rewritten again (#826).
    • Vendored rewrites drop splat, constant and method-call version arguments that Ruby rejected after path: (#847).
    • Hosted mode refuses gems from git sources (gitlab:, custom git_source, string-keyed "git" =>) and lock GIT/PATH sections, and keeps string-keyed options (#652).
    • Hosted mode refuses a redirect a Bundler mirror (mirror.all, a mirror for the patch source, Bundler 4.1 quoted keys) would bypass, with a remedy, and hosted VEX checks existing pins against mirrors (#681).
    • Gem readers use the lock Bundler loads, including gems.locked (#736).
    • The crawler finds Bundler 4 standalone installs in ./bundle (#796), honors path.system over a leftover vendor/bundle (#915), and VEX checks an out-of-tree .bundle/config path for stale copies (#709).
    • Agent apply, rollback, apply --check and vex handle every platform build of a gem version (ffi-1.17.2-x86_64-linux-gnu), the copy RubyGems actually loads (#1092).
    • A BUNDLE_GEMFILE that names the project's Gemfile through a symlinked directory (macOS /tmp, a linked workspace) is recognized in every mode (#896).
    • Hosted unwinds keep the ! Bundler writes for a gem declared in the user's own source … do block, so frozen installs keep working (#1056).
    • A hosted scan of a Gemfile with no lock no longer pins whatever version the shared gem home holds (redirect_gem_no_lockfile) (#1125).
    • Gem checks ignore system gem homes Bundler never loads for the project (deployment, explicit path) (#1098, #1109).
    • With BUNDLE_GEMFILE pointing into another directory (Appraisal-style gemfiles/rails_7.gemfile), agent mode, vex and the stale-install check probe the stores Bundler actually uses (#952).
    • Vendored revert no longer half-reverts when the wired Gemfile line was commented out (#988).
  • PyPI.

    • A Pipenv project without a Pipenv venv no longer falls back to the system Python (agent patched system packages; vendored failed) (#504, #947).
    • Pipenv venv discovery honors settings from .env (or PIPENV_DOTENV_LOCATION) and patches ./.venv too where older Pipenv still uses it (#546, #645).
    • get <name>, socket.yml package lists and scan --package match PyPI names by PEP 503 form, so typing_extensions or ruamel.yaml match (#910, #926).
    • Hosted rollback/remove restore a requirements.txt made up only of hosted pins (#410) and pip-written pylock.toml files (#804).
    • Vendored uv revert writes the specifier pyproject.toml declares now, so uv sync --locked keeps working after a requirement edit (#840).
    • Hosted Poetry and PDM scans rewrite each lock in one pass, which makes scans with many patches several times faster (#760, #762).
    • Every requirements.txt pin is read by one grammar, so vendoring accepts the legacy six (==1.16.0) form that scan and hosted mode did (#1365). Lock-only discovery also looks up equivalent PEP 440 spellings (==1.16, ==1.16.0.0), so those pins get patched on a fresh checkout (#604).
    • Hosted mode no longer replaces a user's own direct reference (name @ <url> to a mirror, fork or file:// path) in requirements.txt (redirect_requirements_direct_reference) (#542), and neither uv writer touches a PEP 508 direct reference in pyproject.toml (#767).
    • uv: reverts handle a vendored package later added directly (#1374), each PEP 723 script / pylock pair separately (#1285, #890), and a package whose parent was uv removed (#1287); hosted unwinds keep the user's own override-dependencies pin (#411); and a scan from a workspace member uses the workspace root's uv.lock (#1138).
    • Hatch-derived pylock.toml files are no longer rewritten alone, since Hatch regenerates them from pyproject.toml (redirect_hatch_lock_regenerated) (#479).
    • PDM restores of a static_urls lock from a private index are refused instead of pointing the lock at files.pythonhosted.org (#413).
    • A vendored revert kept because another file still installs the wheel is reported as a residual reference, not drift, with a remedy that works (#1184). After an unwind, PDM, uv and Pipenv projects get the correct reinstall command for removing the patched build (#477).
    • Vendored Pipfile.lock edits change only the patched entry (keeping \uXXXX escapes and a leading BOM) (#1128).
    • A Poetry dependency pinned to source = "pypi" is refused in a lock written by Poetry 1.4–2.2, which can't install the rewired lock (redirect_poetry_lock_unsupported / pypi_poetry_source_constraint); other Poetry versions are rewired with a warning (#1282).
    • Venvs created with --system-site-packages (including Poetry's and Pipenv's options for it) are crawled through to the base interpreter's packages, so vex no longer attests a lock pin while an unpatched base copy is imported, and stale-install guards warn about it (#409).
    • Hosted and vendored uv refuse to wire through [tool.uv.sources] when no-sources = true or no-sources-package makes uv ignore it (redirect_uv_project_unsupported, pypi_uv_no_sources) (#564).
    • Vendored Hatch refuses (pypi_hatch_unsupported) a checkout whose path contains %, #, ; or ?, which Hatch's {root:uri} turns into a broken file URL (#547).
  • Vendoring. Vendored mode refuses to replace a symlinked lockfile or manifest (redirect_symlinked_file_unsupported) instead of detaching it from its target (#627).

  • General.

    • Rollback and remove delete directories (and stale __pycache__) that the patch created (#838).
    • Crawler probes of version-manager shims (gem, python3, npm, composer, …) time out after 10 s instead of hanging every crawling command (#845).
    • The report-only scan -g / --global-prefix hint keeps the global flag (#464).
    • vex product detection reads Go's block-form module ( … ) directive (#781).

Maintenance

  • Shared format models, project snapshots, ledger views, and a vendored backend consolidate discovery and lifecycle handling. Grouped writes and bounded concurrency reduce repeated disk and network work.
  • CI reuses compiled test binaries and splits broader compatibility matrices into dedicated jobs. Release publishing uses separate Cargo and npm workflows.
  • Documentation now separates usage, configuration, migration, compatibility, and development guidance; completed plans, prototype research, and historical run reports are removed from the maintained docs.
  • Further consolidation of purl identity, lockfile and XML parsing, BOM and line-ending handling, path normalization, digests and atomic writes; faster bun.lock, Poetry and PDM scans. CI runs through a merge queue with sharded, decoupled platform jobs and retries for registry test downloads. rustls is bumped to 0.23.45.
  • Help text, the README and the usage and migration guides are shorter and describe lockfile-first patching, the package-target grammar and service-only vendoring. vendor --force is documented as bypassing only the installed-variant probe; vendor_content_mismatch_overwritten is no longer emitted.
  • Obsolete local-build vendoring machinery, the qbsdiff dependency and duplicate lock readers are removed; Pipfile.lock, nuget.config, purl classification and BOM handling each go through one shared reader.
  • Linux CI runs on Depot runners with a lean per-package-manager gate on PRs and the merge queue (the full matrix runs nightly), reuses the merge queue's verdict on main, and spawns test binaries in a hermetic environment.

[4.0.0] — 2026-08-20

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): apply patches 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 hooks setup configures. Requires socket-patch (and Socket API access, or pre-fetched blobs) on every machine that installs dependencies.

  • Vendored mode (vendor, or scan --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, so vendor --revert restores 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 a goproxy registry 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).

Removed (BREAKING)

  • The unlock subcommand. Folded into repair, which now deletes the leftover <.socket>/apply.lock file as its final housekeeping step (skipped under --dry-run, refused with lock_held while 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 — so unlock's inspect path had no recovery scenario, and its --release file 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 on errorCode: lock_held. SOCKET_UNLOCK_RELEASE is gone with the subcommand, and the patch_unlocked / patch_unlock_failed telemetry events are retired.
  • The global --break-lock flag and SOCKET_BREAK_LOCK env 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 a lock_broken audit event for a reclaim that plain acquisition performs anyway. The lock_broken warning event and rollback's warnings[] lock_broken entry are no longer emitted (warnings stays present, now always empty). The lock_held stderr hint now advises waiting / --lock-timeout instead of pointing at the removed commands.

Changed (BREAKING)

  • --help command order is now workflow-first: scan, apply, vex, vendor, setup, then rollback, get, list, remove, repair.

Added

  • 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 as scan: 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 additive not_installed status 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 hosted now redirects golang dependencies when the reference carries a goproxy registry override: a fork-style replace <mod> <ver> => patch.socket.dev/gopatch/<uuid> <ver>-socketpatch.<n> in go.mod plus the socket module's two h1: lines in go.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 from go.sum, so the committed pair is the whole redirect — validated end-to-end in e2e_golang_hosted_build.rs (fresh caches, bogus GOSUMDB tripwire, go mod tidy byte-level no-op, tampered-hash SECURITY ERROR). Fails closed (per-dep redirect_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 historical redirect_golang_unsupported warning (paid tier stays vendored — see docs/ecosystems.md#go-directory-replaces-and-gosum). Wire schema gains integrity.goModH1 and registryOverride.identifiers.goModuleVersion (additive). Requires server-side publication of the grant-free gopatch artifact 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> --pr performs the whole bump chore — stamps every packaging site via version-sync.sh, rolls [Unreleased] into a dated ## [X.Y.Z] CHANGELOG section, and opens the release/vX.Y.Z PR (also dispatchable from the Actions tab as the Version Bump workflow). A new release-readiness CI job runs scripts/release-lint.sh on 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. The Release workflow's version job 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 published SHA256SUMS before extraction, sanity-execs the staged binary, and atomically swaps it in place (Windows uses the rename-dance via self-replace; a setuid/setgid install is refused). --update 3.4.0 (or SOCKET_PATCH_VERSION) pins a version, up or down; bare --update never downgrades; --force reinstalls. --dry-run is a check-only probe (zero downloads, updateAvailable in the --json details). 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; --force overrides. --offline refuses up front and --force cannot 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 with SOCKET_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 --update refreshes the notice's cache.

  • shellcheck scripts/install.sh in CI (and a fix for the SC2144 glob-with--e musl-loader probe it found).

  • socket login now 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 for apiToken, defaultOrg, and apiBaseUrl: precedence per key is CLI flag > env var

    socket-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 canonical SOCKET_* names win. Two new env-only toggles: SOCKET_NO_API_TOKEN ignores ambient tokens (env + config; an explicit --api-token still authenticates) and SOCKET_NO_CONFIG disables the config layer. A corrupt config file warns once on stderr and is ignored; --json stdout is unaffected. The telemetry endpoint now resolves the API base through the same chain as client construction, so a config-supplied apiBaseUrl applies to both. Design notes: docs/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), scan rewrites 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 rewrites package-lock.json/npm-shrinkwrap.json resolved+integrity (v2 legacy dependencies mirror included), pnpm-lock.yaml inline resolutions, and yarn classic resolved/integrity blocks; pypi rewrites requirements.txt pins to name @ <url> --hash=sha256:… (pip-compile continuation lines are refused rather than corrupted) and uv.lock wheel entries; cargo defines a per-patch sparse registry in .cargo/config.toml plus Cargo.toml registry = keys and Cargo.lock source/checksum surgery; composer rewrites the lock entry's dist url/shasum; nuget adds a nuget.config source + packageSourceMapping and repins packages.lock.json contentHash; gem adds a per-dep source block + a CHECKSUMS pin (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 under tests/fixtures/redirect/. JSON output gains a redirect sub-object with mode: "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 --mode with a boolean of a DIFFERENT mode is a usage error (exit 2); the same mode spelled both ways is accepted, and --detached now requires vendored mode in either spelling.

  • VEX support for hosted mode: the (redirected) provenance marker + the redirect ledger. scan --mode hosted persists 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-run scan --mode hosted --vex attests confirmed redirects from the ledger WITHOUT hash verification (the bytes are fetched at install time; the JSON vex summary carries verified: false), while a post-install socket-patch vex reads the ledger back and hash-verifies the redirected patches against the installed tree. A confirmed redirect whose record fetch failed surfaces a record_fetch_failed warning (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 a nuget.config source + packageSourceMapping and a packages.lock.json contentHash repin — dotnet restore --locked-mode then fails NU1403 on tamper. Maven: the uuid dir is a committed maven2 file:// repository (rebuilt .jar + the verbatim upstream pom so transitives survive + .sha1 sidecars), wired via a pom.xml <repository> with checksumPolicy=fail; multi-module aggregator poms (vendor_maven_multimodule_unsupported) and gradle-only projects (vendor_gradle_unsupported) are refused fail-closed, and the always-on vendor_maven_local_cache_shadow advisory carries the mvn dependency:purge-local-repository one-liner (a warm ~/.m2 copy silently shadows any repository). Both are proven by docker capstones against the real .NET SDK / Apache Maven (cold-cache, --network none, RED + TAMPER probes). nuget and maven are now DEFAULT compile features; the SOCKET_EXPERIMENTAL_NUGET / SOCKET_EXPERIMENTAL_MAVEN runtime 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 covers nuget and maven dirs, and --vendor-source prebuilt 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 injected socket-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.config resolver args (originAware=false, failIfMissing=false) + .mvn/checksums/checksums.sha256 entries 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 a redirect_maven_same_gav_fallback warning (NOT fail-closed: a Socket-repo failure falls back to the unpatched artifact). Gradle build scripts are never edited: a present build.gradle* / settings.gradle* emits a paste-able exclusiveContent snippet 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 and checksum: becomes the precomputed yarnBerry10c0 cache-zip sha512 — leaving the descriptor key and package.json untouched, so yarn install --immutable --check-cache passes and tamper fails YN0018. Whole-file gates refuse a cacheKey ≠ 10c0 or a .yarnrc.yml compressionLevel ≠ 0 (redirect_yarn_berry_cache_unsupported) — no offline-reproducible checksum. Validated e2e against real corepack yarn@4.12.0 on the node-modules linker; PnP is not exercised for hosted (the lock rewrite fires, but PnP's .yarn/cache resolution is untested). bun (text bun.lock v1): 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-lockfile then installs the hosted bytes and tamper fails the integrity check. A binary bun.lockb with no text lock is auto-migrated first via the user's own bun install --save-text-lockfile --frozen-lockfile --lockfile-only (deletes bun.lockb, recorded as a removed ledger edit, offline, fails closed; redirect_bun_lockb_would_migrate on --dry-run, redirect_bun_lockb_unsupported if 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 at common/config/rush/pnpm-lock.yaml (plus one per subspace under common/config/subspaces/<name>/). scan --mode hosted discovers those locks when rush.json is present and repoints them in place (the pnpm rewriter is now basename-generalized, so nested locks rewrite path-generically). Editing a Rush lock outside rush update desyncs the pnpmShrinkwrapHash in common/config/rush/repo-state.json, so a redirect_rush_repo_state_stale warning fires when a lock was touched and that file exists — rush install fails under preventManualShrinkwrapChanges until rush update refreshes 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) because rush install copies the lock into common/temp, so vendor's relative file: specs can't survive — the refusal routes to hosted mode.

  • pnpm hosted rewriter generalized to nested lockfiles. The pnpm-lock.yaml rewriter now matches any pnpm-lock.yaml at 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 GOPROXY chain would leak licensed bytes / tokened URLs to the public mirror. The full analysis lives in docs/ecosystems.md#go-directory-replaces-and-gosum; both the CLI rewriter and the depscan backend twin emit redirect_golang_unsupported naming 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.

  • vendor now supports every major npm and pypi package manager. The npm ecosystem gained four lockfile flavors beyond package-lock.json — yarn classic (yarn.lock v1), yarn berry with the node-modules linker (resolutions + a cache-zip 10c0 checksum reproduced offline from the vendored tarball), pnpm (pnpm.overrides + pnpm-lock.yaml surgery, 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 binary bun.lockb are 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 optional CHECKSUMS section; composer's dist.reference carries the patch UUID into installed.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/--revert accept empty env vars (SOCKET_FORCE=) as false, matching the global-flag contract.

  • New vendor subcommand: committable vendoring of patched dependencies. Where apply patches installed packages in place (machine-local state), socket-patch vendor 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. Per ecosystem (each mechanism validated against the real package manager): npm rewrites package-lock.json only (deterministic patched tarball, recomputed integrity, npm ci-verified); cargo writes a [patch.crates-io] entry in .cargo/config.toml plus surgical Cargo.lock edits so cargo build --locked --offline works; golang reuses the replace-directive engine pointed at the vendor tree; composer rewrites the lock entry to a dist: path copy; gem edits the Gemfile + Gemfile.lock pair in bundler's canonical form; pypi rebuilds a valid wheel (regenerated RECORD) wired through uv's pyproject.toml/uv.lock pair (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.json ledger records the verbatim original lockfile fragments, and vendor --revert restores them byte-exactly. vendor --vex mirrors apply --vex; VEX generation attests vendored patches by hashing the committed artifacts, and apply yields ownership of vendored packages (vendored skip reason).

  • Cargo support (cargo is now a default feature). apply patches a Rust dependency in place wherever the crawler finds it — the project vendor/ directory or the shared $CARGO_HOME registry cache — rewriting the crate's .cargo-checksum.json sidecar so cargo build accepts the modified files. rollback restores the original bytes from the beforeHash blobs, like npm/PyPI/gem. cargo ships on by default (alongside the always-on npm + PyPI

    • Ruby gems support), so released binaries and a plain cargo install socket-patch-cli patch Rust dependencies out of the box; maven/composer/nuget/deno remain opt-in.
  • 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 fail go.sum at build time. Instead apply writes a project-local patched copy under .socket/go-patches/<module>@<version>/ and a managed replace directive in the project go.mod, so the patch is project-scoped and the cache stays pristine for sibling projects. rollback cleanly drops the replace directive + copy. apply --check is 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 apply and scan via --vex <path>. A single successful apply/scan can now both patch and emit the OpenVEX 0.2.0 attestation, instead of requiring a separate socket-patch vex step. The --vex-product / --vex-no-verify / --vex-doc-id / --vex-compact flags mirror the standalone vex knobs (and reuse the SOCKET_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-level vex summary ({ 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.

Changed

  • install.sh can install without reaching github.com. New SOCKET_PATCH_BASE_URL points the archive downloads at any releases base that answers GitHub's two asset paths — notably https://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 --update can use the same host today through the SOCKET_UPDATE_BASE_URL override it already has. Also new: SOCKET_PATCH_INSTALL_DIR to choose the install directory explicitly instead of taking /usr/local/bin or ~/.local/bin. The default download origin is still GitHub — see docs/installer-hosting.md.

  • The documented one-liner installs from https://install.socket.dev/patch. The previous URL was raw.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-byte scripts/install.sh, with its SHA-256 published at install.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 its SHA256SUMS — 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 an installer-drift workflow that checks the hosted copy against this repository weekly.

  • Maven and NuGet promoted to fully available — the SOCKET_EXPERIMENTAL_MAVEN / SOCKET_EXPERIMENTAL_NUGET runtime gates are retired. Every flow (scan in 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 default scan now walks the local Maven repository (~/.m2 / MAVEN_REPO_LOCAL) and the NuGet caches, and scan --prune/--sync now 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 in docs/ecosystems.md: agent-mode patching leaves Maven's .jar.sha1/.jar.md5 stale 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 separate release-ecosystems.yml workflow is removed (its release: published trigger never fired: the release is created with GITHUB_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-url no 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, and deno Cargo 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-features no longer produces a minimal binary (there is nothing left to strip). The SOCKET_EXPERIMENTAL_MAVEN / SOCKET_EXPERIMENTAL_NUGET runtime 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 gates docker-e2e and setup-e2e on socket-patch-cli. (MAJOR for anyone scripting --features; no behavior change for default builds beyond composer/deno support now being present.)

  • Token-less scan now batch-queries the public proxy. Proxy-mode scans POST {proxy}/patch/batch (one request per --batch-size chunk, mirroring the authenticated /v0/orgs/{slug}/patches/batch endpoint) instead of issuing one GET /patch/by-package/:purl per 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 as pkg: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)

Fixed

  • bun 1.4 lockfiles are accepted again. bun 1.4.0 bumped bun.lock lockfileVersion to 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 fixture npm/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>/gems and flat gems/ stores can coexist under one BUNDLE_PATH root, 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. apply now fans out per copy (per-copy Applied events, each counted in summary.applied), bundle-path roots are crawled in bundler precedence order, and config-sourced roots are contained. The gem_bundle_config_path_ignored warning also prints the skipped path verbatim instead of backslash-escaped.

  • npm @socketsecurity/socket-patch: the ./schema export is now built at publish. The subpath pointed at a gitignored dist/ directory that nothing built during release, so it shipped broken; a prepack script now compiles it as part of npm publish.

  • Release workflow tag-guard and idempotency fixes. The tag-already-exists guard never fired (it ran git rev-parse in a shallow, tagless checkout) — it is now a stateless git ls-remote check 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 packageSourceMapping from scratch now emits a catch-all for pre-existing sources. packageSourceMapping is exclusive — once ANY mapping exists, every package must match some source's pattern or restore hard-fails NU1100. A redirect into a nuget.config with 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 vex previously 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 (reported not_applied, or package_not_found on cache-less CI). Verification now follows the managed replace directive to the committed copy.

  • repair on a hosted-only project is an informational no-op. Hosted (--mode hosted) mode leaves no local artifacts to repair — the lockfiles point at patch.socket.dev URLs, and there is no manifest or vendor ledger. A project whose only .socket/ trace is redirect-state.json (no manifest, no vendor ledger, no vendored lockfile references) previously errored with manifest_not_found (exit 1); it now exits 0 with a redirect_only_project skip pointing at scan --mode hosted. Repair still errors on a bare directory with no traces at all.

[3.2.0] — 2026-05-29

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.

Security

  • Path-traversal in archive extraction. read_archive_to_map (patch/package.rs) validated the raw tar entry path but returned the package/-stripped path, so an entry like package//etc/passwd passed every check and then resolved to an absolute /etc/passwd that Path::join writes 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 a Vec sized from the bsdiff target-size header, which qbsdiff never validates — a tiny hostile delta could claim up to i64::MAX and abort the process. The hint is now clamped to 64 MiB.
  • Evidence-free VEX attestation. verify_patch_record (vex/verify.rs) returned applied for a patch touching zero files, producing a not_affected statement with no on-disk evidence; zero-file records are now omitted (no_files).

Fixed — filesystem safety, atomicity & rollback

  • apply could not write into read-only directories (Go module cache marks dirs 0o555); added a DirWriteGuard that temporarily grants write on the parent dir around the CoW-break + atomic rename and restores its exact mode.
  • apply stripped setuid/setgid bits on every patched file because chown ran after chmod; reordered to chown-before-chmod, plus a parent-dir fsync so 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.
  • rollback used an unsafe in-place write; it now delegates to the hardened apply_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_reader now errors when the streamed byte count disagrees with the declared size.
  • Sidecar writes in read-only caches: the cargo .cargo-checksum.json rewrite and the NuGet .nupkg.metadata delete used bare, non-atomic I/O that failed EACCES in the locked-down registry trees they exist to serve; both now go through the hardened write/DirWriteGuard paths.
  • Blob cleanup (utils/cleanup_blobs.rs) aborted the whole sweep on one dangling symlink and inflated the "checked" count with subdirs/dotfiles; now uses symlink_metadata, skips stat errors, and counts only real blobs.
  • Lock acquisition (patch/apply_lock.rs) mapped every flock error to Held (masking ENOLCK/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.

Fixed — crawlers (on-disk layout & metadata)

  • Composer: normalize the v-prefixed installed.json version 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), treat GOPATH as a path list, and reject malformed/empty module directives.
  • 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 bare 3.11 dirs, and a package with missing/malformed METADATA now falls back to its <name>-<version>.dist-info directory name instead of vanishing.
  • Deno: correct the macOS cache path (~/Library/Caches/deno), honor XDG_CACHE_HOME on Linux, and treat an empty DENO_DIR as 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_dir now follows symlinks, fixing symlinked package-dir discovery across every dir-walking crawler at once.

Fixed — API client, commands & misc

  • API client: honor a --proxy-url override 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_AGENT and telemetry context.version were hardcoded to 1.0/1.0.0; both now derive from CARGO_PKG_VERSION.
  • apply no longer emits a spurious Failed envelope event for a release-variant whose first file is NotFound.
  • UTF-8 safety: get/scan/remove truncated display strings with raw byte slices that panic on multi-byte API text; all use char-safe truncation.
  • Exit codes: setup now exits non-zero (not already_configured) when a package.json fails to parse, and repair exits non-zero and fires failure telemetry on a partial download failure (also gates the offline dry-run "would download" event and threads through bytes_freed).
  • rollback no longer miscounts zero-file records as already-original or double-counts no-ops in dry-run; unlock reports released from a pre-acquire snapshot so a probe-created lock file isn't reported as removed.
  • vex resolves qualified PyPI/Gem/Maven PURLs via the rollback-aware resolver so those patches are no longer dropped as package_not_found.
  • package.json handling: no longer panics on a non-object root or non-object scripts, de-dups overlapping workspace patterns, handles bare */**/deep globs, strips inline YAML comments, and preserves top-level key order (enabled serde_json's preserve_order).
  • Smaller fixes: deterministic list output ordering, case-insensitive fuzzy_match tie-break, json_envelope status-invariant enforcement + oldUuid field, lock_cli sub-second timeout message, blob-fetcher all-skipped formatting, VEX Statement.timestamp made optional per OpenVEX 0.2.0, and VEX git-remote url parsing.

Tests & tooling

  • Hundreds of regression tests added across the patch engine, crawlers, API client, manifest, package.json, VEX, and CLI command layers; the stale repair/python_crawler e2e expectations were updated to the corrected contracts. Full suite green (--features cargo).
  • Added the scripts/study-crates.ts per-file audit harness (with an example prompt config) used to drive this review.

[3.1.0] — 2026-05-26

Added

  • Telemetry coverage for read-side + housekeeping + attestation commands. scan, get, list, setup, repair, unlock, and the new vex command each emit a patch_<action> (and matching *_failed) event through the existing send path, joining the apply/remove/rollback trio that already shipped. The scan event carries per-tier counts (free_patches/paid_patches/can_access_paid), the ecosystems filter, and a fallback_to_proxy flag; get carries uuid/tier/ecosystem/download_mode/fallback_to_proxy.

  • scan + get automatically 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 with fallback_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/vex keep their fail-loud semantics.

  • SOCKET_OFFLINE (airgap mode) now disables telemetry universally. is_telemetry_disabled() honors the same SOCKET_OFFLINE=1|true signal --offline uses for network suppression, so apply (and every future command) no longer attempts a 5-second telemetry POST against https://api.socket.dev when the operator explicitly requested airgap.

Tests

  • New tests/cli/telemetry_e2e.rs end-to-end behavioral coverage: apply/scan/get/list emit telemetry against a wiremock recorder; SOCKET_OFFLINE=1 produces 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_invariants cases 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; scan without --apply is read-only against the manifest and blobs even when an update is detected.

[3.0.0] — 2026-05-22

Breaking

  • --offline semantics unified to strict airgap on every subcommand. Previously meant three different things across apply (strict airgap), repair (skip downloads / cleanup-only), and rollback (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-mode default changed from file to diff to match every other subcommand. Users who need the legacy per-file blob behavior must now opt in with --download-mode file.

  • repair --offline is mutually exclusive with --download-only — passing both exits with code 2.

  • Env vars renamed. The three remaining SOCKET_PATCH_* env vars now use the SOCKET_* prefix:

    • SOCKET_PATCH_PROXY_URL → SOCKET_PROXY_URL
    • SOCKET_PATCH_DEBUG → SOCKET_DEBUG
    • SOCKET_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 --silent and --json because the transition signal must reach scripts and CI logs). Legacy names will be removed in v4.

Added

  • Shared GlobalArgs clap 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 matching SOCKET_* env-var binding with precedence CLI arg > env var > default. See CLI_CONTRACT.md for the full global-arguments table.
  • apply and repair accept --api-url, --api-token, --org via 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 --debug and --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 Release workflow: the workflow fails before tag creation if CHANGELOG.md lacks an entry for the version in Cargo.toml. Blocks every downstream publish (cargo, npm, pypi).

Changed

  • Garbage collection moved out of apply. Use scan --prune, scan --sync, or repair / gc instead. apply is 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) for apply, list, remove, repair. Other subcommands keep their pre-v3 ad-hoc shapes for now; see CLI_CONTRACT.md for migration status.

[2.1.4] — 2026-04-09

  • Release workflow tolerates already-published npm packages so a partial publish can be retried without re-tagging.

[2.1.3] — 2026-04-08

  • Pin Node 22.22.1 in the release workflow to dodge a broken upstream npm.

[2.1.2] — 2026-04-08

  • Harden core error handling, blob verification, and --force reporting.
  • Surface find_by_purls errors instead of silently swallowing them.
  • Add diagnostics to apply for silent no-op failures in CI.
  • Add explicit Node typings for TypeScript 6 compatibility in the npm wrapper.

[2.1.1] — 2026-04-02

  • Simplify release to workflow_dispatch only (no bot commits).
  • Split release into PR-based version prep + auto-publish on dispatch.
  • Prioritize pnpm-workspace.yaml detection and restrict setup to root package.json for pnpm monorepos.
  • Harden GitHub Actions workflows per zizmor audit.
  • Unflag Ruby gem (gem) support and add e2e bundler tests.
  • Use npx @socketsecurity/socket-patch for the generated postinstall command.

[2.1.0] — 2026-03-10

  • Full glibc/musl support across all Linux architectures (16 platform combinations now published per release).

[2.0.0] — 2026-03-06

  • Interactive prompts and smart patch selection when multiple patches match a query.

[1.7.1] — 2026-03-06

  • Ensure the binary has execute permission in the PyPI wrapper.
  • Restore bin and optionalDependencies to the npm wrapper package.json.

[1.7.0] — 2026-03-06

  • Expand ecosystem support: rough-in for composer, go, maven, nuget, ruby.
  • Add a TypeScript schema library to the npm wrapper.
  • Treat empty SOCKET_API_TOKEN as unset.

[1.6.3] — 2026-03-05

  • Maintenance release.

[1.6.2] — 2026-03-05

  • Maintenance release (version sync).

[1.6.1] — 2026-03-05

  • Switch to per-platform optionalDependencies for the npm package.
  • Add macOS global-package crawling fallbacks and pyenv support.

[1.6.0] — 2026-03-04

  • Add support for more platforms; fix pypi and npm publish flows.

[1.5.0] — 2026-03-04

  • Fix trusted publishing setup for npm and PyPI.

[1.4.0] — 2026-03-04

  • Update PyPI publish action and add npm provenance permissions.

[1.3.1] — 2026-03-04

  • Fix action image references in the publish workflow.

[1.3.0] — 2026-03-04

  • Add apply --force; rename --no-apply to --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_SLUG is unset.

[1.2.0] — 2026-01-10

  • Fix publish workflow to checkout the bumped version.

[1.1.0] — 2026-01-10

  • Pin GitHub Actions to full commit SHAs and wire up version-bump support in the publish workflow.