diff --git a/README.md b/README.md index 89ee742..ff92517 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,51 @@ # Socket Patch CLI -Apply security patches to your dependencies — npm, PyPI, Cargo, Go, Ruby gems, and more — without waiting for upstream fixes. - -## Choosing a patch mode - -socket-patch delivers the same patched bytes three different ways. The modes differ in *where the patch lives* and *what must happen at install time*; pick one per project (`scan --mode ` drives exactly one mode per run). - -| Mode | Mechanism | User-code changes | CI changes | Offline / airgap | Integrity story | Future GitHub-app default | -|------|-----------|-------------------|------------|------------------|-----------------|---------------------------| -| **hosted** — `scan --mode hosted` | Lockfiles / registry configs are rewritten so **only** the patched dependencies resolve to Socket-hosted, integrity-pinned packages on `patch.socket.dev` | Minimal: lockfile (+ small registry-config) edits; no artifact bytes land in the repo | **None** — your existing install commands pick up the patched packages | ❌ installs must be able to reach `patch.socket.dev` | The package manager's own lockfile verification (sha512 / sha256 / contentHash / CHECKSUMS) pins the hosted bytes. Maven has no lockfile, so it pins by serving the patch under a Socket-only version suffix — fail-closed by repository exclusivity, optionally reinforced with Trusted Checksums (see [Maven & NuGet caveats](#maven--nuget-caveats)) | ✅ planned default for GitHub-app patch PRs — keeps the PR diff small | -| **vendored** — `scan --mode vendored` (or the standalone [`vendor`](#vendor)) | Patched artifacts are committed under `.socket/vendor/` and the lockfile is rewired to consume them | Committed artifacts (the repo grows by the patched package sizes) | **None** | ✅ fully airgapped; no dependency on Socket infrastructure uptime | Committed bytes + recomputed lockfile hashes, verified by the package manager at install time | — | -| **agent** — `scan --mode agent` (or [`apply`](#apply)) | `.socket/manifest.json` + patch blobs are committed; the socket-patch CLI re-applies the patches in CI / postinstall | `.socket/` manifest + blobs committed | **Required** — wire install hooks with [`setup`](#setup) or run `socket-patch apply` in CI | ✅ once blobs are committed (`apply --offline`) | The CLI verifies per-file before/after git-sha256 hashes on every apply | Today's default (the original method) | - -**agent** is the original method and remains fully supported, but it is the only mode that requires CI / install-hook modification — **new projects should prefer hosted or vendored**. - -### Mode × ecosystem support - -| Ecosystem | agent (`--mode agent`) | vendored (`--mode vendored`) | hosted (`--mode hosted`) | -|-----------|------------------------|------------------------------|--------------------------| -| npm (pnpm / yarn / berry / bun) | ✅ any install layout; `setup` postinstall hook | ✅ five lockfile flavors: package-lock, yarn classic, yarn berry (node-modules linker; PnP refused), pnpm v9, bun `bun.lock` (binary `bun.lockb` refused with a `--save-text-lockfile` pointer) | ✅ package-lock / npm-shrinkwrap, pnpm-lock.yaml, yarn classic, **yarn berry** (`yarn.lock` entry only — cacheKey `10c0` / yarn 4, `.yarnrc.yml compressionLevel` must stay 0; node-modules linker is e2e-covered, PnP is untested for hosted — the lock rewrite fires but PnP's `.yarn/cache` resolution isn't exercised), **bun** (text `bun.lock` v1; a binary `bun.lockb` is auto-migrated to text first via your own `bun install --save-text-lockfile`) | -| pypi (uv / poetry / pdm / pipenv / pip) | ✅ `.pth` startup hook via `setup` | ✅ six flavors: uv, poetry, pdm, pipenv, requirements.txt (pip / `uv pip`) | ✅ requirements.txt + uv.lock. **poetry / pdm / pipenv locks are not rewritten** — use vendored | -| cargo | ✅ in-place + `.cargo-checksum.json` rewrite (shared registry-cache caveat — see [`setup`](#setup)) | ✅ `[patch.crates-io]` path entry | ✅ per-patch sparse registry (`[registries.socket-patch-]` + Cargo.lock source/checksum) | -| gem | ✅ Bundler plugin via `setup` | ✅ Gemfile + Gemfile.lock path pair | ✅ per-dep `source` block; the `CHECKSUMS` pin needs bundler ≥ 2.6 (older locks get a `redirect_gem_no_checksums_section` warning) | -| golang | ✅ `go.mod` `replace` → `.socket/go-patches/` | ✅ `replace` → the committed vendor tree | ❌ **not possible** — sumdb, module-path identity, and default-GOPROXY leakage each rule it out; see [docs/design/golang-hosted-no-go.md](docs/design/golang-hosted-no-go.md). **Use vendored** (`redirect_golang_unsupported` names the remedy) | -| maven | ⚠️ experimental — gated behind `SOCKET_EXPERIMENTAL_MAVEN=1` (in-place jar patching corrupts the `~/.m2` checksum sidecars); prefer vendored / hosted | ✅ **new** — committed maven2 `file://` repository. A root pom declaring `` (multi-module aggregator) is refused (`vendor_maven_multimodule_unsupported`), and a gradle-only project is refused (`vendor_gradle_unsupported`) | ✅ **pom projects only, fail-closed** — the patched jar is served under a Socket-only `-socket.` suffix, so the rewriter pins that version (rewrite the literal ``, or add a `` entry for a transitive) alongside the `` insert (`checksumPolicy=fail`). An outage or tamper on the Socket repo then hard-fails the build — the suffixed version exists nowhere else, so there is no silent fall-through to Central. Optionally emits Maven 3.9+ Trusted Checksums files pinning the jar + pom sha256. `${property}` versions are refused. Gradle builds get a paste-able `exclusiveContent` snippet (`redirect_gradle_manual_snippet`); no build script is edited. See [Maven & NuGet caveats](#maven--nuget-caveats) | -| nuget | ⚠️ experimental — gated behind `SOCKET_EXPERIMENTAL_NUGET=1` (in-place patching breaks the `.nupkg.sha512` tamper-evidence sidecar); prefer vendored / hosted | ✅ **new** — committed folder feed + `packageSourceMapping` + `packages.lock.json` contentHash pin | ✅ `nuget.config` source + source-mapping, `packages.lock.json` contentHash rewrite. See the locked-mode note in [Maven & NuGet caveats](#maven--nuget-caveats) | -| composer | ✅ post-install script events | ✅ `composer.lock` `dist: path` rewrite | ✅ `composer.lock` dist url + shasum rewrite | - -> **Maven / NuGet discovery gate**: discovering *installed* Maven and NuGet packages (the crawl behind `scan` / `apply` / `vendor`) currently requires the same `SOCKET_EXPERIMENTAL_MAVEN=1` / `SOCKET_EXPERIMENTAL_NUGET=1` opt-in in every mode. The vendored/hosted wiring itself is safe — the gate guards the agent-mode sidecar risk. - -> **Rush monorepos** (npm): a Rush repo has no root `package.json`/lockfile pair — its single pnpm source-of-truth lock lives at `common/config/rush/pnpm-lock.yaml` (plus one per subspace under `common/config/subspaces//`). **Hosted** ✅ — `scan --mode hosted` discovers and repoints those locks in place (subspaces included). **Agent** ✅ works through the generated project symlink farm. **Vendored** is refused (`vendor_rush_unsupported`): `rush install` copies the lock into `common/temp` and runs pnpm there, so vendor's relative `file:` specs can't survive the copy — the refusal routes you to hosted mode. Editing a Rush lock outside `rush update` desyncs the `pnpmShrinkwrapHash` in `common/config/rush/repo-state.json`, so when `preventManualShrinkwrapChanges` is enabled `rush install` fails until `rush update` refreshes it (a `redirect_rush_repo_state_stale` warning flags this; the redirect survives the refresh — pnpm keeps locked resolutions for unchanged specifiers). - -### Maven & NuGet caveats - -Honest limits of the maven and nuget flows — documented behavior, not bugs: - -* **Fail-closed by version suffixing (hosted maven).** Maven has no lockfile, so hosted mode pins the patch a different way: the Socket serve route exposes the patched jar under a globally-unique `-socket.` suffix that exists **only** on the injected `socket-patch-` repository. The rewriter pins that suffixed version explicitly — it rewrites the literal ``, or (for a transitive / managed dependency with no literal version in your pom) adds a `` entry — so a resolver that can't reach the Socket repo, or is handed different bytes, has nowhere to fall through to: the build **hard-fails** instead of silently resolving the unpatched upstream artifact. The ``'s `checksumPolicy=fail` still verifies the transport-level `.jar.sha1` sidecar on top. A `${property}` version is refused (`redirect_maven_dep_unpinned`) — a literal edit would break the property reference and a depMgmt pin could strand sibling artifacts sharing the property. A literal version that matches neither the base nor the suffixed value is skipped (`redirect_maven_dep_version_mismatch`). -* **Trusted Checksums reinforcement (hosted maven, Maven 3.9+).** When the serve route supplies both the jar and pom sha256, the rewriter also emits Maven [Trusted Checksums](https://maven.apache.org/resolver/expected-checksums.html) files — `.mvn/maven.config` resolver args plus `.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 and surfaces `redirect_maven_trusted_checksums_conflict`). This is an **independent client-side content pin** on top of the transport check. It requires **Maven 3.9+** (the resolver post-processor and the `${session.rootDirectory}` basedir expression the config uses); on older Maven the `.mvn/*` files are silently inert — the version-suffixing above is still fail-closed on its own. On Maven **3.9.0–3.9.8** a *mismatch* is enforced but reported unclearly; the readability fix landed in **3.9.9** ([MNG-8182](https://issues.apache.org/jira/browse/MNG-8182)). The args are `originAware=false` and `failIfMissing=false`, so one checksum matches the artifact from any repository and a dependency with no committed checksum still resolves — only a *mismatch* fails. -* **Warm `~/.m2` shadowing (vendored maven only).** Maven consults the *local repository* before any configured ``, so with vendored mode a warm `~/.m2` copy of the same GAV silently wins over the committed `file://` repository — the build succeeds with **unpatched** bytes. Purge it with: - `mvn dependency:purge-local-repository -DmanualInclude=:` - (the always-on `vendor_maven_local_cache_shadow` warning carries the same one-liner). Hosted mode is **not** affected: the patched jar lives at the suffixed version, which no warm `~/.m2` entry can hold. -* **`mirrorOf` mirrors (hosted maven).** A `settings.xml` `` with `*` (common in corporate environments) reroutes *all* repositories — including the injected `socket-patch-` repository — through the mirror. Because the patch resolves only at the suffixed version, the mirror (which does not carry it) can't serve it and the **build fails loudly** rather than silently going unpatched. Scope the mirror to exclude the Socket repos (e.g. `*,!socket-patch-*`) so the redirect resolves; the `originAware=false` Trusted Checksums act as a backstop when present. -* **Gradle (hosted maven).** Gradle build scripts are never edited. A present `build.gradle*` / `settings.gradle*` gets a paste-able `exclusiveContent { … }` snippet (a `redirect_gradle_manual_snippet` warning) that carries the **suffixed** version — and you must bump the `groupId:artifactId` dependency declaration to that suffixed version yourself. It is fail-closed by repository exclusivity: the `exclusiveContent` filter routes only the suffixed version to the Socket repo, which is the only place it exists. -* **NuGet locked mode (hosted + vendored).** With a `packages.lock.json` and `dotnet restore --locked-mode`, the rewritten `contentHash` pins the patched `.nupkg` — a tampered or wrong package fails restore with `NU1403`. Without a lockfile there is no client-side content pin (vendored surfaces this as a `vendor_nuget_no_lockfile` warning; the feed + source mapping still force the patched copy). +Fix known vulnerabilities in the dependencies you already have — without waiting for an +upstream release, and without a risky version bump. + +Socket's security team backports minimal fixes to the *exact versions* of packages you +have installed. The `socket-patch` CLI finds which of your dependencies have a patch +available and applies it, verifying every changed file by hash. It works across npm, +PyPI, Cargo, Go, RubyGems, Maven, Composer, NuGet, and Deno, and it can persist the patches +whichever way fits your workflow: re-applied by the CLI, committed to your repo, or +pinned in your lockfile. When you're done, it can emit an [OpenVEX +attestation](#openvex-attestations) so your vulnerability scanner stops flagging the +CVEs you've already fixed. + +**Contents:** [Installation](#installation) · [Five-minute tutorial](#five-minute-tutorial) +· [How it works](#how-socket-patch-works) · [Common tasks](#common-tasks) +· [Command reference](#command-reference) · [OpenVEX](#openvex-attestations) +· [Scripting & CI/CD](#scripting--cicd) · [Manifest format](#manifest-format) +· [Ecosystem support →](docs/ecosystems.md) ## Installation -### One-line install (recommended) +One-line install (macOS / Linux): ```bash curl -fsSL https://raw.githubusercontent.com/SocketDev/socket-patch/main/scripts/install.sh | sh ``` -Detects your platform (macOS/Linux, x64/ARM64), downloads the latest binary, and installs to `/usr/local/bin` or `~/.local/bin`. Use `sudo sh` instead of `sh` if `/usr/local/bin` requires root. +Detects your platform (macOS/Linux, x64/ARM64), downloads the latest binary, and installs +to `/usr/local/bin` or `~/.local/bin`. Use `sudo sh` instead of `sh` if `/usr/local/bin` +requires root. + +On Windows, install via npm (below) or grab a prebuilt `socket-patch-*-pc-windows-msvc.zip` +from the [latest release](https://github.com/SocketDev/socket-patch/releases/latest). + +Or install through your package manager: + +| Package manager | Command | +|-----------------|---------| +| npm | `npm install -g @socketsecurity/socket-patch` (or one-shot: `npx @socketsecurity/socket-patch`) | +| pip | `pip install socket-patch` | +| cargo | `cargo install socket-patch-cli` (builds from source with every ecosystem compiled in) | +| gem | `gem install socket-patch` | +| composer | `composer require socketsecurity/socket-patch` (run as `vendor/bin/socket-patch`) | + +The gem and composer packages are thin launchers: on first run they download the prebuilt +binary for your platform from the matching GitHub release, verify its SHA-256, cache it, +and exec it. Set `SOCKET_PATCH_BIN` to an existing binary to skip the download.
Manual download @@ -70,115 +63,319 @@ curl -fsSL https://github.com/SocketDev/socket-patch/releases/latest/download/so curl -fsSL https://github.com/SocketDev/socket-patch/releases/latest/download/socket-patch-x86_64-unknown-linux-musl.tar.gz | tar xz # Linux (ARM64) -curl -fsSL https://github.com/SocketDev/socket-patch/releases/latest/download/socket-patch-aarch64-unknown-linux-gnu.tar.gz | tar xz +curl -fsSL https://github.com/SocketDev/socket-patch/releases/latest/download/socket-patch-aarch64-unknown-linux-musl.tar.gz | tar xz ``` +The musl builds are fully static and run on any distro; glibc (`-gnu`) variants are also +on the releases page, alongside Windows (`socket-patch-x86_64-pc-windows-msvc.zip`) and +other targets. + Then move the binary onto your `PATH`: ```bash sudo mv socket-patch /usr/local/bin/ ``` +The full list of prebuilt targets (Windows, 32-bit ARM, i686, Android) is in +[docs/ecosystems.md](docs/ecosystems.md#supported-platforms). +
-### npm +## Five-minute tutorial + +No account or token is needed to follow along — without an API token `socket-patch` +talks to Socket's public patch proxy, which serves the free tier of patches anonymously. +(An API token unlocks your organization's patch tier; if you've already run +`socket login` with the separate [Socket CLI](https://docs.socket.dev/docs/socket-cli), +`socket-patch` picks it up automatically — see +[Configuration sources](#configuration-sources).) + +**1. Scan your project.** From your project root, ask Socket which of your installed +dependencies have patches available: ```bash -npx @socketsecurity/socket-patch +cd your-project +socket-patch scan ``` -Or install globally: +`scan` crawls the installed packages it finds (`node_modules/`, virtualenvs, the cargo +registry cache, and so on), queries the patch database, prints each available patch with +its package, severity, and CVE/GHSA identifiers, and asks whether to apply. Say yes and +the vulnerable files are rewritten in place — each file is hash-verified before and after +the edit. + +> If it prints `No patches available for installed packages.`, none of your installed +> dependency versions currently has a Socket patch — the good outcome, with nothing to +> apply. (One exception: Maven and NuGet installed-package discovery is experimental and +> off by default — `scan` silently skips them unless `SOCKET_EXPERIMENTAL_MAVEN=1` / +> `SOCKET_EXPERIMENTAL_NUGET=1` is set; see the +> [mode × ecosystem matrix](docs/ecosystems.md#mode--ecosystem-matrix).) +> To walk the rest of the loop anyway, make a scratch project pinned to a version +> that has a free patch — at the time of writing, `flatted@3.3.1`: +> +> ```bash +> mkdir demo && cd demo && git init -q && npm init -y && npm install flatted@3.3.1 && socket-patch scan +> ``` +> +> (The patch catalog changes over time; if that finds nothing, pick another patched +> version.) + +**2. See what you have.** The applied patches are recorded in `.socket/manifest.json`: ```bash -npm install -g @socketsecurity/socket-patch +socket-patch list ``` -### pip +``` +Found 1 patch(es): -```bash -pip install socket-patch +Package: pkg:npm/flatted@3.3.1 + UUID: 5cac955f-eab1-4d29-8f4f-c408a6cc9647 + ... + Vulnerabilities (1): + - GHSA-25h7-pfq9-p65f (CVE-2026-32141) + Severity: HIGH ``` -### Cargo +**3. Make it stick.** Patches applied in place don't survive a reinstall — the next +`npm install` (or `pip install`, `bundle install`, …) restores the vulnerable upstream +bytes. Commit the `.socket/` directory and wire an install hook so patches re-apply +automatically: ```bash -cargo install socket-patch-cli +socket-patch setup # e.g. adds a postinstall script for npm projects +echo '.socket/apply.lock' >> .gitignore # lock state, not part of the patch record +git add .gitignore .socket package.json # npm example — setup prints which files it changed +git commit -m "apply Socket security patches" ``` -This builds with every supported ecosystem (npm, PyPI, Ruby gems, Cargo, -Go, Maven, Composer, NuGet, Deno) compiled in. +From now on, every install — yours, your teammates', CI's — re-applies the patches. You +can also re-apply manually at any time with `socket-patch apply` (it's idempotent). -### RubyGems +**4. Undo, if you want.** Remove a patch completely (restores the original files and +deletes the manifest entry): ```bash -gem install socket-patch +socket-patch remove "pkg:npm/flatted@3.3.1" ``` -A thin launcher gem: on first run it downloads the prebuilt binary for your -platform from the matching GitHub release, verifies its SHA-256, caches it, and -execs it. Set `SOCKET_PATCH_BIN` to an existing binary to skip the download. +That's the whole loop: **scan → apply when prompted → setup → commit**. This tutorial +used the default *agent* mode, where the CLI re-applies patches after each install. +There are two other ways to persist patches — committing the patched packages themselves +(*vendored*) or pinning them in your lockfile (*hosted*) — and choosing between the +three is the next section. + +## How Socket Patch works + +**A patch** is a minimal fix — usually the upstream security fix, backported — for one +exact published version of a package. Socket distributes it as per-file edits: for each +touched file, the hash of the expected original (`beforeHash`), the hash of the patched +result (`afterHash`), and the replacement content. By default, a file whose current +content matches neither the expected original nor the patched result is overwritten with +the full verified patched content plus a stderr warning (`content_mismatch_overwritten`); +pass `--strict` (a [global option](#global-options)) to fail closed on mismatch instead, +or `apply --force` to skip pre-application hash verification entirely (see +[`apply`](#apply)). Either way the CLI verifies the result after writing. Patches are +looked up by package URL ([PURL](https://github.com/package-url/purl-spec)) — e.g. +`pkg:npm/lodash@4.17.20` — so everything is keyed to exact versions. + +**Local state lives in `.socket/`** at your project root, and is designed to be +committed: + +| Path | Contents | +|------|----------| +| `.socket/manifest.json` | The record of downloaded patches: PURLs, file hashes, vulnerability metadata ([format](#manifest-format)) | +| `.socket/blobs/` | Patched file contents, named by git-sha256 hash | +| `.socket/vendor/` | Vendored package artifacts and the vendor/redirect ledgers (only in vendored/hosted modes) | + +> Mutating commands also leave a `.socket/apply.lock` file there between runs. It is +> lock state, not part of the patch record — add it to your `.gitignore` +> ([`repair`](#repair) deletes it). + +### Three patch modes + +The same patched bytes can reach your build three different ways. The modes differ in +*where the patch lives* and *what must happen at install time*; pick one per project +(`scan --mode ` drives exactly one mode per run). + +| Mode | Where the patch lives | Install-time requirement | Trade-off | +|------|----------------------|--------------------------|-----------| +| **agent** — `scan --mode agent` (or [`apply`](#apply)) | `.socket/` manifest + blobs, committed; the CLI re-applies after each install | The `socket-patch` CLI must run (install hook via [`setup`](#setup), or an `apply` step in CI) | Small repo footprint (per-file blobs, not whole packages); no lockfile edits; the only mode that needs CI / install-hook changes | +| **vendored** — `scan --mode vendored` (or [`vendor`](#vendor)) | Patched packages committed under `.socket/vendor/`; the lockfile is rewired to consume them | **None** — the package manager installs the committed bytes | Fully airgapped and hermetic, at the cost of repo size | +| **hosted** — `scan --mode hosted` | No patched bytes in your repo: the lockfile is rewritten so **only** the patched dependencies resolve to Socket-hosted, integrity-pinned packages on `patch.socket.dev`; the edits + patch records are ledgered in `.socket/vendor/redirect-state.json` (commit it — [`vex`](#vex) reads it, and it records the pre-redirect originals a future revert feature will need; hosted has no CLI revert yet, see [Undo things](#undo-things)) | Installs must be able to reach `patch.socket.dev` (no CLI, no install hook) | Smallest possible diff (lockfile + ledger); not for airgapped installs | + +Every mode pins the patched bytes: in agent mode the CLI verifies every file on each +apply; vendored and hosted modes lean on your package manager's own lockfile integrity +checks (sha512 / sha256 / contentHash / CHECKSUMS) where the ecosystem enforces them — +hosted Maven, which has no lockfile, gets a fail-closed version-suffixing scheme instead. +A few combinations have weaker install-time pins (vendored Maven, NuGet without a +lockfile, Go's directory replaces, pipenv's Pipfile.lock) — there the committed bytes +are the protection; see the [per-ecosystem caveats](docs/ecosystems.md). + +**Choosing:** *agent* is the original method and remains fully supported, but it is the +only mode that requires CI / install-hook modification — **new projects should prefer +hosted or vendored**. Pick *vendored* if your builds are airgapped or you don't want an +infrastructure dependency; pick *hosted* if you want the smallest diff and your installs +can reach `patch.socket.dev`. (Hosted is the planned default for GitHub-app patch PRs — +it keeps the PR diff small.) + +Mode support varies by ecosystem — e.g. Go can't do hosted, Rush monorepos can't do +vendored. See the full **[mode × ecosystem matrix](docs/ecosystems.md#mode--ecosystem-matrix)** +for details and per-ecosystem caveats. + +## Common tasks + +### Patch everything that can be patched -### Composer +```bash +socket-patch scan # interactive: prompts before applying +socket-patch scan --json --mode agent --yes # non-interactive (CI, scripts) +``` + +### Patch one specific CVE, advisory, or package ```bash -composer require socketsecurity/socket-patch -vendor/bin/socket-patch --help +socket-patch get CVE-2024-12345 +socket-patch get GHSA-xxxx-yyyy-zzzz +socket-patch get lodash # fuzzy-matches installed packages +socket-patch get "pkg:npm/lodash@4.17.20" ``` -Same launcher model as the RubyGems package (download-on-first-run, cached, -`SOCKET_PATCH_BIN` to bypass). +`socket-patch ` with a bare patch UUID is a shortcut for `get `. + +### Keep patches applied across installs + +```bash +socket-patch setup # wire install hooks (npm postinstall, Python .pth, …) +socket-patch setup --check # CI gate: exit non-zero if hooks are missing or a patch drifted +``` -## Quick Start +See [`setup`](#setup) for what gets wired per ecosystem — and which ecosystems (Cargo, +Go, Maven, NuGet, Deno) have no hook and are patched on demand instead. -You can pass a patch UUID directly to `socket-patch` as a shortcut: +### Persist patches with no CI or install-hook changes (vendored / hosted) ```bash -socket-patch 550e8400-e29b-41d4-a716-446655440000 -# equivalent to: socket-patch get 550e8400-e29b-41d4-a716-446655440000 +# Vendored: commit the patched packages themselves (airgap-friendly) +socket-patch scan --json --mode vendored --yes +echo '.socket/apply.lock' >> .gitignore +git add .gitignore .socket package-lock.json # your lockfile may differ + +# Hosted: smallest diff — patched deps resolve from patch.socket.dev +socket-patch scan --json --mode hosted --yes +git add .socket/vendor/redirect-state.json package-lock.json ``` -## Global Options +No `setup` hook or CI `apply` step is needed — the package manager installs the patched +bytes. See [Three patch modes](#three-patch-modes) to choose, and the +[mode × ecosystem matrix](docs/ecosystems.md#mode--ecosystem-matrix) for what your +ecosystem supports. + +### Run an auto-update bot in CI -These flags are accepted by **every** subcommand — they are flattened into each command's argument set, so `socket-patch --json --cwd ./app` works uniformly. A command silently ignores any global flag it doesn't use (e.g. `list --global` parses fine and the flag is a no-op). +One command discovers, applies, and garbage-collects in a single pass: -Each flag has a matching `SOCKET_*` environment variable. **Precedence is CLI arg > env var > default**, so a flag on the command line always wins over the environment. +```bash +socket-patch scan --json --mode agent --prune --yes +``` -### Configuration sources +The working-tree changes (the `.socket/` directory — plus lockfile edits if your bot +runs `--mode vendored` or `--mode hosted`) are what your PR tooling commits — e.g. +`peter-evans/create-pull-request` picks them up automatically; use the JSON summary for +the PR title/body. See [Scripting & CI/CD](#scripting--cicd), including how to supply +`SOCKET_API_TOKEN` for org-tier patches. -For the three authentication settings, the [Socket CLI](https://docs.socket.dev/docs/socket-cli)'s persisted login sits between the env var and the built-in default — run `socket login` (or `socket config set apiToken` / `defaultOrg`) once and socket-patch picks it up too. Resolution is per key, and an empty value means "unset" at every layer: +### Tell your vulnerability scanner about the patches +```bash +socket-patch vex --output socket.vex.json +grype --vex socket.vex.json # or trivy image --vex ... ``` ---api-token / --org / --api-url - 1. CLI flag - 2. Env var SOCKET_API_TOKEN / SOCKET_ORG_SLUG / SOCKET_API_URL - 3. Peer alias env SOCKET_CLI_API_TOKEN / SOCKET_CLI_ORG_SLUG / SOCKET_CLI_API_BASE_URL - 4. socket-cli config /socket/settings/config.json — read-only - (Linux: ~/.local/share; macOS: ~/Library/Application Support - then legacy ~/.local/share, both after $XDG_DATA_HOME; - Windows: %LOCALAPPDATA%) - 5. Built-in default no token → public proxy; org → auto-resolve; https://api.socket.dev + +The OpenVEX document marks each patched CVE `not_affected`, so scanners stop flagging +vulnerabilities you've already remediated. You can also emit it inline from `apply` / +`scan` / `vendor` with `--vex `. Details in [OpenVEX +attestations](#openvex-attestations). + +### Work offline / airgapped + +Vendored mode needs no Socket infrastructure and no `socket-patch` binary at install +time — the patched packages install from the committed bytes (other, unvendored +dependencies still resolve from your registry or mirror as usual). Agent mode works +offline once the blobs are committed: + +```bash +socket-patch apply --offline # strict airgap: fails loudly if anything needs the network ``` -Two env-only toggles adjust this: `SOCKET_NO_API_TOKEN=1` ignores ambient tokens (env + config; an explicit `--api-token` still wins), and `SOCKET_NO_CONFIG=1` disables the config-file layer entirely. socket-patch never *writes* the config file, and a corrupt one only produces a stderr warning — it never breaks a command or pollutes `--json` output. socket-patch does **not** read `.env` files or any per-repository config for endpoints or credentials: a cloned repo must never be able to redirect where patches come from or spend your token. +`scan` and `get` inherently need the network and refuse to run with `--offline`. + +### Undo things + +Five commands clean up different layers — the first three undo, the last two reconcile +and repair; pick by what you want back: + +| Command | What it does | +|---------|--------------| +| [`rollback`](#rollback) | Restores the original file bytes but **keeps the manifest entry** — the next `apply` re-applies the patch | +| [`remove`](#remove) | Everything `rollback` does, **plus** it deletes the manifest entry and reverts any vendoring — **permanent**, the patch is fully gone in one command | +| [`vendor --revert`](#vendor) | **Un-vendors wholesale**: restores the recorded original lockfile fragments byte-for-byte and removes the `.socket/vendor/` artifacts — works without a manifest | +| [`scan --prune`](#scan) | **Reconciles, doesn't reverse**: drops manifest entries for packages that have left the project and garbage-collects orphan blob/diff/archive files — installed patches stay | +| [`repair`](#repair) (alias `gc`) | **Restores health, not originals**: re-downloads missing blobs, rebuilds missing/corrupt vendored artifacts, cleans up unused ones, and removes the leftover `apply.lock` file (housekeeping — mutating commands leave it behind after every run) | + +And `setup --remove` reverts the install hooks that `setup` added. + +> Hosted mode has no CLI revert yet: `scan --mode hosted` makes plain lockfile / +> registry-config edits, so undo them with your version control (e.g. +> `git checkout -- `) and delete the `.socket/vendor/redirect-state.json` +> ledger — once you've reverted by hand, its recorded original fragments are stale, and +> a leftover ledger would still let [`vex`](#vex) attest the removed redirects. + +## Command reference + +| Command | What it does | +|---------|--------------| +| [`scan`](#scan) | Scan installed packages for available security patches | +| [`apply`](#apply) | Apply security patches from the local manifest | +| [`vex`](#vex) | Generate an OpenVEX attestation for the applied patches | +| [`vendor`](#vendor) | Eject patched dependencies into committable `.socket/vendor/` | +| [`setup`](#setup) | Wire install hooks so patches re-apply automatically | +| [`rollback`](#rollback) | Restore original files (keeps the manifest) | +| [`get`](#get) | Fetch and apply a patch by UUID / CVE / GHSA / PURL / name (alias: `download`) | +| [`list`](#list) | List all patches in the local manifest | +| [`remove`](#remove) | Remove a patch: roll back files + delete the manifest entry | +| [`repair`](#repair) | Download missing blobs, clean up unused ones, tidy lock state (alias: `gc`) | + +### Global options + +These flags are accepted by **every** subcommand and go after the command name — +`socket-patch --json --cwd ./app` works uniformly (`socket-patch --json +` is a parse error). A command silently ignores any global flag it doesn't use +(e.g. `list --global` parses fine and the flag is a no-op). + +Each flag has a matching `SOCKET_*` environment variable, listed in the table; +command-specific flags list theirs in each command's own table. **Precedence is CLI arg +> env var > default** — with one extra fallback layer for the three authentication +settings, described in [Configuration sources](#configuration-sources) below. | Flag | Env var | Description | |------|---------|-------------| | `--cwd ` | `SOCKET_CWD` | Working directory (default: `.`). The manifest path is resolved relative to this. | | `--manifest-path ` | `SOCKET_MANIFEST_PATH` | Path to the patch manifest, resolved relative to `--cwd` (default: `.socket/manifest.json`). | | `--api-url ` | `SOCKET_API_URL` | Socket API URL for the authenticated endpoint (default: `https://api.socket.dev`). | -| `--api-token ` | `SOCKET_API_TOKEN` | Socket API token. When omitted, the public patch proxy is used. | +| `--api-token ` | `SOCKET_API_TOKEN` | Socket API token — optional. When no token resolves from any source, the anonymous public patch proxy is used (free patches). See [Configuration sources](#configuration-sources) for how to obtain and persist one. | | `-o, --org ` | `SOCKET_ORG_SLUG` | Organization slug. Auto-resolved when omitted and a token is set. | -| `--proxy-url ` | `SOCKET_PROXY_URL` | Public proxy URL used when no API token is set. | -| `-e, --ecosystems ` | `SOCKET_ECOSYSTEMS` | Restrict to specific ecosystems (comma-separated, e.g. `npm,pypi`). | +| `--proxy-url ` | `SOCKET_PROXY_URL` | Public proxy URL used when no API token is set (default: `https://patches-api.socket.dev`). | +| `-e, --ecosystems ` | `SOCKET_ECOSYSTEMS` | Restrict to specific ecosystems (comma-separated, e.g. `npm,pypi`). Unknown names are rejected. | | `--download-mode ` | `SOCKET_DOWNLOAD_MODE` | Artifact to fetch when local files are missing: `diff` (default, smallest delta), `package` (full per-package tarball), or `file` (legacy per-file blobs). | -| `--vendor-source ` | `SOCKET_VENDOR_SOURCE` | How `vendor` acquires the installable artifact: `auto` (default — download the prebuilt package from patch.socket.dev, fall back to a local build on any miss), `service` (require the service, fail-closed), or `build` (always build locally). Covers npm, pypi, cargo, golang, composer, gem, and nuget (maven attempts the prebuilt download under `auto` but is not covered by fail-closed `service`). | +| `--vendor-source ` | `SOCKET_VENDOR_SOURCE` | How `vendor` acquires the installable artifact: `auto` (default — download the prebuilt package from patch.socket.dev, fall back to a local build on any miss), `service` (require the service, fail-closed), or `build` (always build locally). Covers npm, pypi, cargo, golang, composer, gem, nuget, and maven. | | `--vendor-url ` | `SOCKET_VENDOR_URL` | Base host for the vendoring service's package-reference request (default: the active `--api-url`/`--proxy-url` base). Point at staging / local dev for testing. | | `--patch-server-url ` | `SOCKET_PATCH_SERVER_URL` | Override the host of the prebuilt-archive download URL the service returns (default: as returned). Mainly for local-dev / testing. | | `--offline` | `SOCKET_OFFLINE` | Strict airgap: never contact the network. Operations that need remote data fail loudly. | | `--strict` | `SOCKET_STRICT` | Fail-closed on before-hash mismatches instead of the default warn-and-overwrite: a file whose current content matches neither `beforeHash` nor `afterHash` aborts that package's apply. Overridden by `--force`. | | `-g, --global` | `SOCKET_GLOBAL` | Operate on globally-installed packages. | | `--global-prefix ` | `SOCKET_GLOBAL_PREFIX` | Override the path used to discover globally-installed packages. | -| `-j, --json` | `SOCKET_JSON` | Emit machine-readable JSON output. Every JSON response includes a `"status"` field — camelCase on the envelope commands (`"success"`, `"error"`, `"noManifest"`, `"partialFailure"`; apply/list/repair/remove/vendor), snake_case on the legacy shapes (`"partial_failure"`, `"not_found"`; get/scan/rollback/setup). See [CLI_CONTRACT.md](crates/socket-patch-cli/CLI_CONTRACT.md) for the exact shapes. | +| `-j, --json` | `SOCKET_JSON` | Emit machine-readable JSON output. Every JSON response includes a `"status"` field — camelCase on the envelope commands (`"success"`, `"error"`, `"noManifest"`, `"partialFailure"`, `"paidRequired"`, `"notFound"`; apply/list/repair/remove/vendor), snake_case on the legacy shapes (`"partial_failure"`, `"not_found"`; get/scan/rollback/setup). See [CLI_CONTRACT.md](crates/socket-patch-cli/CLI_CONTRACT.md) for the exact shapes. | | `-v, --verbose` | `SOCKET_VERBOSE` | Show extra detail in human-readable output. | | `-s, --silent` | `SOCKET_SILENT` | Suppress non-error output. | | `--dry-run` | `SOCKET_DRY_RUN` | Preview the operation without making any mutations. | @@ -187,33 +384,86 @@ Two env-only toggles adjust this: `SOCKET_NO_API_TOKEN=1` ignores ambient tokens | `--debug` | `SOCKET_DEBUG` | Emit verbose debug logs to stderr. | | `--no-telemetry` | `SOCKET_TELEMETRY_DISABLED` | Disable anonymous usage telemetry. | -## Commands +#### Configuration sources -The tables below list only the **command-specific** flags. Every command also accepts the [Global Options](#global-options) above. +For the three authentication settings, the [Socket CLI](https://docs.socket.dev/docs/socket-cli)'s +persisted login sits between the env var and the built-in default — run `socket login` +(or `socket config set apiToken` / `defaultOrg`) once and `socket-patch` picks it up +too. The `SOCKET_CLI_*` env vars the JS CLI reads are honored as peer aliases as well, +so one export configures both tools. To set a token directly instead, create one in the +[Socket dashboard](https://socket.dev) under your organization's API tokens settings and +use the raw token (`sktsec_<...>_api`) shown at generation time, **not** the +`sha512-...` display hash. Resolution is per key, and an empty value means "unset" at +every layer: + +``` +--api-token / --org / --api-url + 1. CLI flag + 2. Env var SOCKET_API_TOKEN / SOCKET_ORG_SLUG / SOCKET_API_URL — or the + SOCKET_CLI_* peer aliases (SOCKET_CLI_API_TOKEN / + SOCKET_CLI_ORG_SLUG / SOCKET_CLI_API_BASE_URL); the + canonical name wins when both are set + 3. socket-cli config /socket/settings/config.json — read-only + (Linux: $XDG_DATA_HOME, else ~/.local/share; + macOS: $XDG_DATA_HOME, else ~/Library/Application Support, + then legacy ~/.local/share; Windows: %LOCALAPPDATA%) + 4. Built-in default no token → public proxy; org → auto-resolve; + url → https://api.socket.dev +``` + +Two env-only toggles adjust this. `SOCKET_NO_API_TOKEN=1` ignores ambient tokens (env + +config; an explicit `--api-token` still wins) — useful to force the anonymous public +proxy in CI or a test run. `SOCKET_NO_CONFIG=1` disables the config-file layer entirely. +`socket-patch` never *writes* the config file, and a corrupt one only produces a stderr +warning — it never breaks a command or pollutes `--json` output. `socket-patch` does +**not** read `.env` files or any per-repository config for endpoints or credentials: a +cloned repo must never be able to redirect where patches come from or spend your token. +(Full rationale: [docs/design/configuration.md](docs/design/configuration.md).) + +The sections below list only each command's **command-specific** flags. ### `scan` -Scan installed packages for available security patches. `scan --mode agent --prune` is the single command bots need for full auto-update: it discovers patches, applies them, and garbage-collects orphan blob files plus manifest entries for uninstalled packages — all in one invocation. +Scan installed packages for available security patches — and, with `--mode`, act on what +it finds. `scan` is the entry point for all three [patch modes](#three-patch-modes): + +- `--mode agent` downloads and applies the selected patches in place; +- `--mode vendored` discovers, downloads, and builds + wires the committable + `.socket/vendor/` artifacts in one pass (re-vendoring automatically when a newer patch + is selected); +- `--mode hosted` rewrites lockfiles / registry configs so only the patched dependencies + resolve to Socket-hosted packages. -`scan` is also the entry point for all three [patch modes](#choosing-a-patch-mode): `--mode agent` applies in place, `--mode vendored` commits artifacts, `--mode hosted` rewrites lockfiles to Socket-hosted packages. +Without a mode, interactive `scan` prompts before applying, and `scan --json` is +read-only (discovery plus an `updates[]` array; no mutation). + +`scan --mode agent --prune` is the single command bots need for full auto-update: it +discovers patches, applies them, and garbage-collects orphan blob files plus manifest +entries for uninstalled packages — all in one invocation. **Usage:** ```bash socket-patch scan [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `--mode ` | The selector for the three [patch modes](#choosing-a-patch-mode). `agent` downloads and applies selected patches in place (non-interactive; without it, `scan --json` is read-only); `vendored` discovers, downloads, and builds + wires the committable `.socket/vendor/` artifacts in one pass (re-vendors automatically when a newer patch is selected); `hosted` rewrites lockfiles / registry configs so **only** the patched dependencies resolve to Socket-hosted, integrity-pinned packages (no artifact bytes land in the repo; the recorded edits + patch records go to the `.socket/vendor/redirect-state.json` ledger so a post-install [`vex`](#vex) can attest them). Combining `--mode` with a legacy boolean flag of a *different* mode is an error (exit 2); the same mode spelled both ways is accepted. Legacy boolean spellings (`--apply` == agent, `--vendor` == vendored, `--sync` == agent + prune) remain supported for back-compat. | -| `--prune` | Garbage-collect after the scan: remove manifest entries for uninstalled packages and orphan blob/diff/package-archive files. Off by default. [Vendored](#vendor) packages are never pruned. Orthogonal to `--mode` — combines with any mode. | -| `--detached` | With `--mode vendored`: skip all `.socket/manifest.json` writes — the vendor ledger embeds the patch records instead. For projects that want the vendored patches *only* in the lockfile + `.socket/vendor/`. | -| `--batch-size ` | Packages per API request (default: `100`) | -| `--all-releases` | Store patches for every release/distribution variant, not just the installed one — makes the manifest portable across environments (e.g. cross-platform CI caches) | -| `--vex ` | On a successful scan, also write an OpenVEX 0.2.0 document to this path. See [Inline VEX generation](#inline-vex-on-apply--scan--vendor). (env: `SOCKET_VEX`) | -| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | Passthrough to the embedded VEX builder; mirror the standalone [`vex`](#vex) knobs. Inert unless `--vex` is set. | - -> Use `--dry-run` to preview what a `--mode agent` / `--mode vendored` / `--mode hosted` run (with or without `--prune`) would do without mutating disk. +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `--mode ` | — | Selects one of the three [patch modes](#three-patch-modes), summarized above. Combining `--mode` with a legacy boolean flag of a *different* mode is an error (exit 2); the same mode spelled both ways is accepted. | +| `--prune` | — | Garbage-collect after the scan: remove manifest entries for packages no longer present in the crawl (installed trees + lockfiles — a wiped `node_modules` alone doesn't prune lockfile-listed entries) and delete orphan blob/diff/package-archive files. Off by default. [Vendored](#vendor) packages are exempt from the crawl-based prune (an absent installed copy is their normal state), but a vendored entry whose dependency has left the lockfile is reverted and its manifest entry dropped. Orthogonal to `--mode` — combines with any mode. | +| `--detached` | — | With `--mode vendored`: skip all `.socket/manifest.json` writes — the vendor ledger embeds the patch records instead. For projects that want the vendored patches *only* in the lockfile + `.socket/vendor/`. Detached patches are invisible to `apply`/`rollback`/`repair`; undo them with `remove ` or `vendor --revert`. | +| `--batch-size ` | `SOCKET_BATCH_SIZE` | Packages per API request (default: `100`). | +| `--all-releases` | `SOCKET_ALL_RELEASES` | Store patches for every release/distribution variant, not just the installed one — PyPI wheel/sdist, RubyGems platform, Maven classifier. Makes the manifest portable across environments (e.g. cross-platform CI caches). | +| `--vex ` | `SOCKET_VEX` | On a successful scan, also write an OpenVEX 0.2.0 document to this path. See [Inline VEX generation](#inline-vex-on-apply--scan--vendor). | +| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | `SOCKET_VEX_*` | Passthrough to the embedded VEX builder; mirror the standalone [`vex`](#vex) knobs. Inert unless `--vex` is set. | + +> Deprecated boolean spellings of `--mode` remain supported for back-compat: `--apply` +> (== `--mode agent`) and `--vendor` (== `--mode vendored`); prefer `--mode`. `--sync` +> is not deprecated — it is convenience sugar for `--mode agent` + `--prune`, the +> single-flag bot invocation (`scan --json --sync --yes`). + +> Use `--dry-run` to preview what any moded run (with or without `--prune`) would do +> without mutating disk. **Examples:** ```bash @@ -226,7 +476,7 @@ socket-patch scan --json # Agent mode: discover + apply patches in place (non-interactive) socket-patch scan --json --mode agent --yes -# Bot mode: discover, apply, prune, sweep — all in one +# Auto-update bot: discover, apply, garbage-collect — all in one socket-patch scan --json --mode agent --prune --yes # Preview an agent-mode + prune run without mutating disk @@ -258,25 +508,27 @@ socket-patch scan --json --mode vendored --yes --dry-run socket-patch scan --json --mode hosted --yes ``` -> Already-vendored packages are **skipped by plain `--mode agent`** (the committed artifact -> is the patch); a newer available patch still appears in the JSON `updates[]` array — re-run -> `scan --mode vendored` to take it. +> Already-vendored packages are **skipped by plain `--mode agent`** (the committed +> artifact is the patch); a newer available patch still appears in the JSON `updates[]` +> array — re-run `scan --mode vendored` to take it. ### `apply` -Apply security patches from the local manifest. +Apply security patches from the local manifest. Idempotent — safe to run from install +hooks and CI on every build. **Usage:** ```bash socket-patch apply [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `-f, --force` | Skip pre-application hash verification (apply even if package version differs) | -| `--vex ` | On a successful apply, also write an OpenVEX 0.2.0 document to this path. See [Inline VEX generation](#inline-vex-on-apply--scan--vendor). (env: `SOCKET_VEX`) | -| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | Passthrough to the embedded VEX builder; mirror the standalone [`vex`](#vex) knobs. Inert unless `--vex` is set. | +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `-f, --force` | `SOCKET_FORCE` | Skip pre-application hash verification (apply even if package version differs). | +| `--check` | — | Read-only audit that the committed **Go** `replace`-redirects match the manifest (for CI / GitHub-App auditing) — Go only, since cargo patches in place and has no redirect to audit. Lock-free, crawl-free, and offline-safe: exits 0 in sync, 1 on drift. Vendored modules are excluded from the audit. | +| `--vex ` | `SOCKET_VEX` | On a successful apply, also write an OpenVEX 0.2.0 document to this path. See [Inline VEX generation](#inline-vex-on-apply--scan--vendor). | +| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | `SOCKET_VEX_*` | Passthrough to the embedded VEX builder; mirror the standalone [`vex`](#vex) knobs. Inert unless `--vex` is set. | **Examples:** ```bash @@ -300,26 +552,28 @@ socket-patch apply --vex socket.vex.json ``` > Packages managed by [`vendor`](#vendor) are skipped (`skipped`/`vendored` in JSON): the -> committed vendored artifact is the patch, so there is nothing for `apply` to do — even when -> the installed tree (e.g. `node_modules/`) is absent. +> committed vendored artifact is the patch, so there is nothing for `apply` to do — even +> when the installed tree (e.g. `node_modules/`) is absent. ### `vex` -Generate an [OpenVEX](https://github.com/openvex) 0.2.0 attestation describing the vulnerabilities that the applied patches have mitigated. See [OpenVEX attestations](#openvex-attestations) below for the full workflow. +Generate an [OpenVEX](https://github.com/openvex) 0.2.0 attestation describing the +vulnerabilities that the applied patches have mitigated. See [OpenVEX +attestations](#openvex-attestations) below for the full workflow. **Usage:** ```bash socket-patch vex [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `-O, --output ` | Write the VEX document to this path instead of stdout. Required when combined with `--json`. (env: `SOCKET_VEX_OUTPUT`) | -| `--product ` | Override the auto-detected top-level product PURL/identifier. (env: `SOCKET_VEX_PRODUCT`) | -| `--no-verify` | Skip the on-disk file-hash check and trust the manifest — useful on a build machine that doesn't have the patched files laid out. (env: `SOCKET_VEX_NO_VERIFY`) | -| `--doc-id ` | Override the document `@id`. Default is a random `urn:uuid:` regenerated each run; pin this for a reproducible identifier. (env: `SOCKET_VEX_DOC_ID`) | -| `--compact` | Emit compact JSON instead of pretty-printed. (env: `SOCKET_VEX_COMPACT`) | +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `-O, --output ` | `SOCKET_VEX_OUTPUT` | Write the VEX document to this path instead of stdout. Required when combined with `--json`. | +| `--product ` | `SOCKET_VEX_PRODUCT` | Override the auto-detected top-level product PURL/identifier. | +| `--no-verify` | `SOCKET_VEX_NO_VERIFY` | Skip the on-disk file-hash check and trust the manifest — useful on a build machine that doesn't have the patched files laid out. | +| `--doc-id ` | `SOCKET_VEX_DOC_ID` | Override the document `@id`. Default is a random `urn:uuid:` regenerated each run; pin this for a reproducible identifier. | +| `--compact` | `SOCKET_VEX_COMPACT` | Emit compact JSON instead of pretty-printed. | **Examples:** ```bash @@ -339,47 +593,47 @@ socket-patch vex --no-verify --output socket.vex.json ### `vendor` `apply`'s **committable** sibling — the standalone command behind -[vendored mode](#choosing-a-patch-mode) (`scan --mode vendored` runs discovery + this engine in -one pass). Instead of patching installed packages in place -(machine-local state), `vendor` ejects each patched package into -`.socket/vendor///…` and rewires your lockfile so the project consumes -the vendored copy. Commit `.socket/vendor/` plus the lockfile edits and **every fresh checkout -builds with the patched dependency** — no `socket-patch` binary, no Socket API access, no +[vendored mode](#three-patch-modes) (`scan --mode vendored` runs discovery + this engine +in one pass). Instead of patching installed packages in place (machine-local state), +`vendor` ejects each patched package into `.socket/vendor///…` and +rewires your lockfile so the project consumes the vendored copy. Commit `.socket/` — the +vendored artifacts plus the manifest that [`vex`](#vex), [`list`](#list), and +[`repair`](#repair) read — along with the lockfile edits, and **every fresh checkout +builds with the patched dependency**: no `socket-patch` binary, no Socket API access, no install hook required on the consuming machine. -Supported ecosystems: **npm** (package-lock / yarn classic / yarn berry / pnpm / bun), **PyPI** -(uv / poetry / pdm / pipenv / requirements.txt), **RubyGems**, **Cargo**, **Go**, -**Composer**, **Maven** (single-module pom projects — multi-module aggregators and gradle-only -projects are refused), and **NuGet**. Vendoring is per-patch: only dependencies with a Socket -patch are vendored. +Vendoring is per-patch: only dependencies with a Socket patch are vendored. For the +lockfile flavors each ecosystem supports, see the +[mode × ecosystem matrix](docs/ecosystems.md#mode--ecosystem-matrix). **Usage:** ```bash socket-patch vendor [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `-f, --force` | Skip pre-vendor hash verification (vendor even if the installed files differ from the patch's `beforeHash`) | -| `--revert` | Undo vendoring: restore the recorded original lockfile fragments byte-for-byte and remove the `.socket/vendor/` artifacts. Works without a manifest | -| `--vex ` | On a successful vendor, also write an OpenVEX 0.2.0 document to this path (env: `SOCKET_VEX`) | -| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | Passthrough to the embedded VEX builder. Inert unless `--vex` is set. | +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `-f, --force` | `SOCKET_FORCE` | Tolerate *missing* patch-target files in the staged copy (skipped instead of failing the vendor) and bypass the variant probe for multi-release ecosystems. A plain before-hash mismatch doesn't need this: vendor staging always overwrites mismatched content with the verified patched bytes (surfaced as a `vendor_content_mismatch_overwritten` warning). | +| `--revert` | `SOCKET_VENDOR_REVERT` | Undo vendoring: restore the recorded original lockfile fragments byte-for-byte and remove the `.socket/vendor/` artifacts. Works without a manifest. | +| `--vex ` | `SOCKET_VEX` | On a successful vendor, also write an OpenVEX 0.2.0 document to this path. | +| `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | `SOCKET_VEX_*` | Passthrough to the embedded VEX builder. Inert unless `--vex` is set. | -**How it interacts with the rest of the CLI** — once a package is vendored, `vendor` owns it: +**How it interacts with the rest of the CLI** — once a package is vendored, `vendor` owns +it: -- [`apply`](#apply) and [`rollback`](#rollback) skip vendored packages (they never touch a - vendor-owned tree or lockfile entry). +- [`apply`](#apply) and [`rollback`](#rollback) skip vendored packages (they never touch + a vendor-owned tree or lockfile entry). - [`remove`](#remove) **reverts the vendoring** as part of removing the patch — lockfile restored, artifact deleted — so one command fully undoes it. -- [`scan`](#scan) skips downloading/applying patches for vendored packages and never prunes - their manifest entries; newer patches show up in `updates[]` as the signal to re-run - `scan --mode vendored`. +- [`scan`](#scan) skips downloading/applying patches for vendored packages, and + `--prune` exempts them from its crawl-based prune (though a vendored entry whose + dependency has left the lockfile is reverted and dropped); newer patches show up in + `updates[]` as the signal to re-run `scan --mode vendored`. - [`vex`](#vex) attests vendored patches by verifying the **committed artifact** (marked - `(vendored)` in the impact statement) — no `setup` install hook needed, because the lockfile - wiring *is* the persistence mechanism. -- Re-running `vendor` is idempotent; patches dropped from the manifest are auto-reverted on the - next run. + `(vendored)` in the impact statement) — no `setup` install hook needed. +- Re-running `vendor` is idempotent; patches dropped from the manifest are auto-reverted + on the next run. **Examples:** ```bash @@ -389,8 +643,9 @@ socket-patch vendor # Preview without writing anything socket-patch vendor --dry-run -# Then make it stick: commit the artifacts and the rewired lockfile -git add .socket/vendor package-lock.json && git commit -m "vendor Socket patches" +# Then make it stick: commit .socket/ (vendor artifacts + manifest) and the lockfile +# (gitignore .socket/apply.lock — see "How Socket Patch works") +git add .socket package-lock.json && git commit -m "vendor Socket patches" # Undo everything (restores the original lockfile byte-for-byte) socket-patch vendor --revert @@ -399,19 +654,60 @@ socket-patch vendor --revert socket-patch vendor --json ``` -> Prefer one command? [`scan --mode vendored`](#scan) discovers, downloads, *and* vendors in a single -> pass. +> Prefer one command? [`scan --mode vendored`](#scan) discovers, downloads, *and* vendors +> in a single pass. ### `setup` -Configure your project so patches are **re-applied automatically after install** — no manual `socket-patch apply` step in CI. `setup` is a one-time operation: run it, commit the change together with your `.socket/` patches, and every later install handles the rest. It is strictly **opt-in** — nothing is hooked unless you run `setup` and commit the result. - -- **npm / yarn / pnpm / bun** — writes a `postinstall` script into `package.json` so any install re-applies patches (pnpm: root package only). -- **Python (pip / uv / poetry / pdm / hatch)** — Python has no universal post-install hook, so `setup` instead commits a **`socket-patch[hook]`** dependency (for classic Poetry, the equivalent `socket-patch = { extras = ["hook"] }`). Installing it lays down a startup `.pth` (shipped by the small `socket-patch-hook` wheel) that re-applies your committed `.socket/` patches the next time the interpreter runs. It is package-manager-agnostic (it rides the interpreter, not any one installer) and **fail-open** — a hook error can never break interpreter startup. -- **Ruby gems (Bundler)** — adds a managed `plugin "socket-patch"` block to the `Gemfile` and commits an in-tree Bundler plugin under `.socket/bundler-plugin/`. It re-applies patches on every `bundle install` (cached *and* fresh). (Requires the `socket-patch` CLI on `PATH`.) -- **Composer (PHP)** — appends `socket-patch apply` to `composer.json`'s `post-install-cmd` / `post-update-cmd` script events, so patches re-apply on every `composer install` / `composer update`. (Requires the `socket-patch` CLI on `PATH`.) -- **Cargo & Go** — *apply-only, no `setup` hook.* A one-click auto-repatch-on-build isn't possible for these, so `setup` skips them. Patch with `socket-patch apply` directly: **cargo** patches the crate in place (in `vendor/` or the registry cache, rewriting `.cargo-checksum.json` so `cargo build` accepts it); **go** writes a project-local patched copy under `.socket/go-patches/` plus a `go.mod` `replace` directive (the module cache is `go.sum`-verified, so in-place patching can't build). Commit `go.mod` + `.socket/go-patches/` so a clone builds the patched bytes. Declare them in `setup.manual` for VEX attestation. -- **Apply-only ecosystems** (nuget · maven · deno) — no native install hook to wire, so `setup` reports `no_files`; patch them on demand with `socket-patch apply`. +Configure your project so patches are **re-applied automatically after install** — no +manual `socket-patch apply` step in CI. `setup` is a one-time operation: run it, commit +the change together with your `.socket/` patches, and every later install handles the +rest. It is strictly **opt-in** — nothing is hooked unless you run `setup` and commit the +result. + +What gets wired, per ecosystem: + +- **npm / yarn / pnpm / bun** — writes `postinstall` and `dependencies` scripts into + `package.json` so any install — including `npm install ` — re-applies patches + (pnpm: root package only). +- **Python (pip / uv / poetry / pdm / hatch)** — Python has no universal post-install + hook, so `setup` instead adds a **`socket-patch[hook]`** dependency to your manifest + (`pyproject.toml` / `requirements.txt`; for classic Poetry, the equivalent + `socket-patch = { extras = ["hook"] }`). Installing it lays down + a startup `.pth` (shipped by the small `socket-patch-hook` wheel) that re-applies your + committed `.socket/` patches the next time the interpreter runs. It is + package-manager-agnostic (it rides the interpreter, not any one installer) and + **fail-open** — a hook error can never break interpreter startup. Details below. +- **RubyGems (Bundler)** — adds a managed `plugin "socket-patch"` block to the `Gemfile` + and generates an in-tree Bundler plugin under `.socket/bundler-plugin/`. It re-applies + patches on every `bundle install` (cached *and* fresh). (Requires the `socket-patch` + CLI on `PATH`.) +- **Composer (PHP)** — appends `socket-patch apply` to `composer.json`'s + `post-install-cmd` / `post-update-cmd` script events, so patches re-apply on every + `composer install` / `composer update`. (Requires the `socket-patch` CLI on `PATH`.) +- **Cargo & Go** — *apply-only, no `setup` hook.* A one-click auto-repatch-on-build isn't + possible for these, so `setup` skips them. Patch with `socket-patch apply` directly: + **cargo** patches the crate in place (in `vendor/` or the registry cache, rewriting + `.cargo-checksum.json` so `cargo build` accepts it) — note that a non-vendored crate + patches the **shared** `$CARGO_HOME/registry` cache, which affects every project on + the machine and is silently reset by `cargo clean` or a cache prune; vendor the + dependency (`--mode vendored`) for a project-local, committable patch. **go** writes a + project-local patched copy under `.socket/go-patches/` plus a `go.mod` `replace` + directive (the module cache is `go.sum`-verified, so in-place patching can't build); + commit `go.mod` + `.socket/go-patches/` so a clone builds the patched bytes. To have + [`vex`](#vex) still attest these hand-applied patches, add a `setup.manual` array to + `.socket/manifest.json` by hand (there is no CLI flag for it yet): + `"setup": { "manual": ["cargo", "golang"] }`. +- **Maven / NuGet / Deno** — also apply-only: no native install hook exists to wire, so + `setup` reports `no_files`; patch them on demand with `socket-patch apply`, and declare + them in `setup.manual` (the same hand-edit as the Cargo & Go note above, e.g. + `"setup": { "manual": ["deno"] }`) so [`vex`](#vex) still attests the hand-applied + patches — this matters most for Deno, which has no vendored or hosted alternative. + For Maven + and NuGet, discovery of installed packages is experimental and off by default (opt in + with `SOCKET_EXPERIMENTAL_MAVEN=1` / `SOCKET_EXPERIMENTAL_NUGET=1`), and in-place + patching corrupts their cache checksum sidecars — prefer `--mode vendored` or + `--mode hosted`; see [ecosystems.md](docs/ecosystems.md#maven--nuget-caveats). **Usage:** ```bash @@ -420,28 +716,43 @@ socket-patch setup --check # verify configured; non-zero exit if not (CI gate socket-patch setup --remove # revert what setup added ``` -**Command-specific options** (plus all [Global Options](#global-options) — `--dry-run`, `--yes`, `--json`, `--cwd`): -| Flag | Description | -|------|-------------| -| `--check` | Read-only verification that every manifest is configured; exits non-zero if any still needs setup. Never writes (safe in CI). Conflicts with `--remove`. | -| `--remove` | Revert the install hooks `setup` added (npm `package.json` scripts, the Python `socket-patch[hook]` dependency, and the gem Bundler plugin wiring). | +**Command-specific options** (plus all [Global options](#global-options) — `--dry-run`, +`--yes`, `--json`, `--cwd` are the most relevant): +| Flag | Env var | Description | +|------|---------|-------------| +| `--check` | — | Read-only verification that every manifest is configured **and** every installed patch is still applied on disk (each file matches its recorded `afterHash`); exits non-zero if any manifest still needs setup or a patch has drifted. Never writes (safe in CI). Conflicts with `--remove`. | +| `--remove` | — | Revert every install hook `setup` added (npm `package.json` scripts, the Python `socket-patch[hook]` dependency, the gem Bundler plugin wiring, and the Composer `post-install-cmd`/`post-update-cmd` script entries). | +| `--exclude ` | `SOCKET_SETUP_EXCLUDE` | Workspace-member path(s) to exclude from setup (comma-separated, relative to the repo root). The exclusion is persisted in `.socket/manifest.json`, so `setup --check` and a fresh clone honor it without re-passing the flag. | #### Disabling / opting out (Python hook) The Python hook is designed to be easy to skip or remove: -- **Per interpreter / CI step:** set `SOCKET_PATCH_HOOK=off` (or `SOCKET_NO_HOOK=1`). This is checked *before any hook code runs*, so it fully bypasses the hook for that process. -- **Remove from a project:** `socket-patch setup --remove`, then `pip uninstall socket-patch-hook`. -- **Never opted in:** if you don't run `setup`, there is no hook — it is opt-in by design. +- **Per interpreter / CI step:** set `SOCKET_PATCH_HOOK=off` (or `SOCKET_NO_HOOK=1`). + This is checked *before any hook code runs*, so it fully bypasses the hook for that + process. +- **Remove from a project:** `socket-patch setup --remove`, then + `pip uninstall socket-patch-hook`. +- **Never opted in:** if you don't run `setup`, there is no hook — it is opt-in by + design. #### What the Python hook does, and its safety model -On interpreter startup, *only when the set of installed packages changed*, the hook runs `socket-patch apply --offline --ecosystems pypi` for the project that owns the current virtualenv, re-applying only the patches committed in that project's `.socket/`. Specifically: - -- It is **anchored to the virtualenv** it is installed in (not the working directory), so a `python` started from an unrelated directory cannot pull in a foreign `.socket/manifest.json`. -- It **verifies each file's hash before patching** and **never writes outside the installed package directory** (path-escaping manifest keys are refused). -- It resolves the `socket-patch` binary from the **installed `socket-patch` package** (not from `PATH`), so an unexpected binary on `PATH` is not executed. -- It runs **offline** (no network at startup) and is **fail-open** (any error is swallowed; it can never abort the interpreter). +On interpreter startup, *only when the set of installed packages changed*, the hook runs +`socket-patch apply --offline --ecosystems pypi` for the project that owns the current +virtualenv, re-applying only the patches committed in that project's `.socket/`. +Specifically: + +- It is **anchored to the virtualenv** it is installed in (not the working directory), so + a `python` started from an unrelated directory cannot pull in a foreign + `.socket/manifest.json`. +- It **verifies each file's hash before patching** and **never writes outside the + installed package directory** (path-escaping manifest keys are refused). +- It **prefers the binary shipped in the installed `socket-patch` package** over `PATH`, + so a binary planted earlier on `PATH` cannot shadow it; `PATH` is consulted only as a + fallback when that package isn't installed. +- It runs **offline** (no network at startup) and is **fail-open** (any error is + swallowed; it can never abort the interpreter). **Examples:** ```bash @@ -454,7 +765,7 @@ socket-patch setup -y # Preview changes socket-patch setup --dry-run -# Verify configuration in CI (exits non-zero if not set up) +# Verify configuration in CI (exits non-zero if not set up or a patch has drifted) socket-patch setup --check # JSON output for scripting @@ -463,17 +774,26 @@ socket-patch setup --json -y ### `rollback` -Rollback patches to restore original files. If no identifier is given, all patches are rolled back. Packages managed by [`vendor`](#vendor) are excluded — their patch lives in the committed artifact, not the installed tree — and are listed in the JSON output's `vendored` array (use `remove` or `vendor --revert` to undo them). +Roll back patches to restore the original files. If no identifier is given, all patches +are rolled back. The manifest entries are kept, so a later `apply` re-applies the patches +— use [`remove`](#remove) to delete a patch permanently. + +Packages managed by [`vendor`](#vendor) are excluded — their patch lives in the committed +artifact, not the installed tree — and are listed in the JSON output's `vendored` array +(use `remove` or `vendor --revert` to undo them). **Usage:** ```bash socket-patch rollback [identifier] [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `--one-off` | Rollback by fetching original (`beforeHash`) files from the API — no manifest required | +**Arguments:** +- `identifier` — package PURL or patch UUID to roll back. Omit to roll back all patches. + +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `--one-off` | `SOCKET_ONE_OFF` | Reserved: rollback by fetching original (`beforeHash`) files from the API, no manifest required. **Not yet implemented** — the command currently errors up front. | **Examples:** ```bash @@ -495,27 +815,36 @@ socket-patch rollback --json ### `get` -Get security patches from Socket API and apply them. Accepts a UUID, CVE ID, GHSA ID, PURL, or package name. The identifier type is auto-detected but can be forced with a flag. +Get a security patch from the Socket API and apply it. Accepts a UUID, CVE ID, GHSA ID, +PURL, or package name. The identifier type is auto-detected but can be forced with a +flag. -Alias: `download` +Alias: `download`. And as a shortcut, `socket-patch ` with a bare patch UUID is +rewritten to `socket-patch get `. **Usage:** ```bash socket-patch get [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `--id` | Force identifier to be treated as a UUID | -| `--cve` | Force identifier to be treated as a CVE ID | -| `--ghsa` | Force identifier to be treated as a GHSA ID | -| `-p, --package` | Force identifier to be treated as a package name | -| `--save-only` | Download patch without applying it (alias: `--no-apply`) | -| `--one-off` | Apply patch immediately without saving to the `.socket` folder | -| `--all-releases` | Download patches for every release/distribution variant of a matched package (PyPI wheel/sdist, RubyGems platform, Maven classifier), not just the installed one | +**Arguments:** +- `identifier` — patch UUID, CVE ID, GHSA ID, package PURL, or package name. Type is + auto-detected; force it with `--id` / `--cve` / `--ghsa` / `--package`. -> Authenticated lookups require an org: pass `--org ` (or set `SOCKET_ORG_SLUG`) when using `SOCKET_API_TOKEN`. +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `--id` | — | Force identifier to be treated as a UUID. | +| `--cve` | — | Force identifier to be treated as a CVE ID. | +| `--ghsa` | — | Force identifier to be treated as a GHSA ID. | +| `-p, --package` | — | Force identifier to be treated as a package name. | +| `--save-only` | `SOCKET_SAVE_ONLY` | Download the patch without applying it (alias: `--no-apply`). | +| `--one-off` | `SOCKET_ONE_OFF` | Reserved: apply the patch immediately without saving to the `.socket` folder. **Not yet implemented** — the command currently errors up front. | +| `--all-releases` | `SOCKET_ALL_RELEASES` | Download patches for every release/distribution variant of a matched package (PyPI wheel/sdist, RubyGems platform, Maven classifier), not just the installed one. | + +> Authenticated lookups run against an org. The slug is auto-resolved from your token +> when omitted; pass `--org ` (or set `SOCKET_ORG_SLUG`) to pick one explicitly — +> useful when the token belongs to multiple orgs. **Examples:** ```bash @@ -550,7 +879,8 @@ List all patches in the local manifest. socket-patch list [options] ``` -No command-specific options — see [Global Options](#global-options) (`--json`, `--manifest-path`, `--cwd` are the relevant ones). +No command-specific options — see [Global options](#global-options) (`--json`, +`--manifest-path`, `--cwd` are the relevant ones). **Examples:** ```bash @@ -561,25 +891,32 @@ socket-patch list socket-patch list --json ``` -**Sample Output:** +**Sample output:** ``` -Found 2 patch(es): +Found 1 patch(es): -Package: pkg:npm/lodash@4.17.20 - UUID: 550e8400-e29b-41d4-a716-446655440000 +Package: pkg:npm/flatted@3.3.1 + UUID: 5cac955f-eab1-4d29-8f4f-c408a6cc9647 Tier: free License: MIT + Exported: Wed, 18 Mar 2026 22:53:26 GMT Vulnerabilities (1): - - GHSA-xxxx-yyyy-zzzz (CVE-2024-12345) - Severity: high - Summary: Prototype pollution in lodash - Files patched (1): - - lodash.js + - GHSA-25h7-pfq9-p65f (CVE-2026-32141) + Severity: HIGH + Summary: flatted vulnerable to unbounded recursion DoS in parse() revive phase + Files patched (6): + - package/cjs/index.js + - package/es.js + ... ``` ### `remove` -Remove a patch from the manifest (rolls back files first by default). If the package is [vendored](#vendor), `remove` also **reverts the vendoring** — the lockfile is restored byte-for-byte and the `.socket/vendor/` artifact is deleted — so the patch is fully gone in one command. Detached-vendored patches (from `scan --mode vendored --detached`) are removable by PURL or UUID too, even though they have no manifest entry. +Remove a patch from the manifest (rolls back files first by default). If the package is +[vendored](#vendor), `remove` also **reverts the vendoring** — the lockfile is restored +byte-for-byte and the `.socket/vendor/` artifact is deleted — so the patch is fully gone +in one command. Detached-vendored patches (from `scan --mode vendored --detached`) are +removable by PURL or UUID too, even though they have no manifest entry. **Usage:** ```bash @@ -587,12 +924,12 @@ socket-patch remove [options] ``` **Arguments:** -- `identifier` - Package PURL (e.g., `pkg:npm/package@version`) or patch UUID +- `identifier` — package PURL (e.g. `pkg:npm/package@version`) or patch UUID. -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `--skip-rollback` | Only update manifest, do not restore original files (for vendored packages this also leaves the vendor wiring + artifact in place) | +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `--skip-rollback` | `SOCKET_SKIP_ROLLBACK` | Only update the manifest, do not restore original files (for vendored packages this also leaves the vendor wiring + artifact in place). | **Examples:** ```bash @@ -615,26 +952,35 @@ Download missing blobs, clean up unused blobs, and reset the advisory lock state Alias: `gc` -`repair` cleans up the `.socket/` directory without running a scan — useful when you've manually adjusted the manifest, recovered from a partial-failure state, or just want to free space. For the combined workflow (discover + apply + GC in one pass), use `scan --mode agent --prune --json --yes` instead. +`repair` cleans up the `.socket/` directory without running a scan — useful when you've +manually adjusted the manifest, recovered from a partial-failure state, or just want to +free space. It also rebuilds missing or corrupt vendored artifacts. For the combined +workflow (discover + apply + GC in one pass), use +`scan --json --mode agent --prune --yes` instead. -As its final step, `repair` removes the leftover `.socket/apply.lock` file that mutating commands retain between runs (skipped under `--dry-run`). A leftover file from a crashed run never blocks anything — the OS releases a dead process's lock automatically — so this is pure housekeeping. If another socket-patch process is actively running, `repair` refuses up front with `lock_held` (exit 1); it never steals a live lock — wait for the other process to finish, or budget a wait with `--lock-timeout`. +As its final step, `repair` removes the leftover `.socket/apply.lock` file that mutating +commands retain between runs (skipped under `--dry-run`). A leftover file from a crashed +run never blocks anything — the OS releases a dead process's lock automatically — so this +is pure housekeeping. If another `socket-patch` process is actively running, `repair` +refuses up front with `lock_held` (exit 1); it never steals a live lock — wait for the +other process to finish, or budget a wait with `--lock-timeout`. **Usage:** ```bash socket-patch repair [options] ``` -**Command-specific options** (plus all [Global Options](#global-options)): -| Flag | Description | -|------|-------------| -| `--download-only` | Only download missing artifacts, do not clean up (incompatible with `--offline`) | +**Command-specific options** (plus all [Global options](#global-options)): +| Flag | Env var | Description | +|------|---------|-------------| +| `--download-only` | `SOCKET_DOWNLOAD_ONLY` | Only download missing artifacts, do not clean up (incompatible with `--offline`). | **Examples:** ```bash # Full repair (download missing + clean up unused) socket-patch repair -# Cleanup only, no downloads +# Cleanup only — missing blobs are warned about and skipped, never downloaded socket-patch repair --offline # Download missing blobs only @@ -644,35 +990,40 @@ socket-patch repair --download-only socket-patch repair --json ``` -### Undoing things - -Five commands undo different layers of socket-patch state — pick by what you want back: - -| Command | What it undoes | -|---------|----------------| -| [`rollback`](#rollback) | Restores the original file bytes but **keeps the manifest entry** — the next `apply` re-applies the patch | -| [`remove`](#remove) | Rollback **plus** deletes the manifest entry and reverts any vendoring — **permanent**, the patch is fully gone in one command | -| [`vendor --revert`](#vendor) | **Un-vendors wholesale**: restores the recorded original lockfile fragments byte-for-byte and removes the `.socket/vendor/` artifacts — works without a manifest | -| [`scan --prune`](#scan) | **Reconciles, doesn't reverse**: drops manifest entries for packages no longer installed and garbage-collects orphan blob/diff/archive files — installed patches stay | -| [`repair`](#repair) (alias `gc`) | **Restores health, not originals**: re-downloads missing blobs, rebuilds missing/corrupt vendored artifacts, cleans up unused ones, and removes the leftover `apply.lock` from a crashed run | - ## OpenVEX attestations -`socket-patch vex` turns your local manifest into a signed-off statement of *which known vulnerabilities no longer affect your build* because a Socket patch has been applied. This lets vulnerability scanners stop flagging CVEs that you've already remediated in place — without bumping the package version. +`socket-patch vex` turns your local manifest into a machine-readable statement of *which +known vulnerabilities no longer affect your build* because a Socket patch has been applied. +This lets vulnerability scanners stop flagging CVEs that you've already remediated in +place — without bumping the package version. **How it works** -1. Reads `.socket/manifest.json` and, unless `--no-verify` is passed, re-checks each patched file's hash on disk so the attestation only covers patches that are actually applied. [Vendored](#vendor) patches are verified against the **committed artifact** instead of the installed tree (their impact statement carries a `(vendored)` marker), and need no `setup` install hook to be attested — the lockfile wiring is the persistence mechanism. Detached-vendored patches (`scan --mode vendored --detached`) attest from the vendor ledger's embedded records, and [hosted-mode](#choosing-a-patch-mode) patches attest from the redirect ledger (`.socket/vendor/redirect-state.json`, marker `(redirected)` — hash-verified against the installed tree post-install), so `vex` works even with no manifest file at all. -2. Auto-detects the top-level **product** identifier (override with `--product`), probing in order: - - `.git/config` `[remote "origin"]` → `pkg:github//` (similar for GitLab/Bitbucket; raw URL otherwise) +1. Reads `.socket/manifest.json` and, unless `--no-verify` is passed, re-checks each + patched file's hash on disk so the attestation only covers patches that are actually + applied. [Vendored](#vendor) patches are verified against the **committed artifact** + instead of the installed tree (their impact statement carries a `(vendored)` marker), + and need no `setup` install hook to be attested. Detached-vendored patches + (`scan --mode vendored --detached`) + attest from the vendor ledger's embedded records, and + [hosted-mode](#three-patch-modes) patches attest from the redirect ledger + (`.socket/vendor/redirect-state.json`, marker `(redirected)` — hash-verified against + the installed tree post-install), so `vex` works even with no manifest file at all. +2. Auto-detects the top-level **product** identifier (override with `--product`), probing + in order: + - `.git/config` `[remote "origin"]` → `pkg:github//` (similar for + GitLab/Bitbucket; raw URL otherwise) - `package.json` → `pkg:npm/@` - `pyproject.toml` → `pkg:pypi/@` - `Cargo.toml` → `pkg:cargo/@` -3. Emits an OpenVEX 0.2.0 document whose statements mark each mitigated vulnerability as `not_affected` (justification: the patch is present), suitable for piping into `vexctl`, Grype, Trivy, and similar tools. +3. Emits an OpenVEX 0.2.0 document whose statements mark each mitigated vulnerability as + `not_affected` (justification: the patch is present), suitable for piping into + `vexctl`, Grype, Trivy, and similar tools. **Provenance markers** -Each statement's impact string records *how* the patch is persisted — one marker per [patch mode](#choosing-a-patch-mode): +Each statement's impact string records *how* the patch is persisted — one marker per +[patch mode](#three-patch-modes): | Impact statement | Mode | What the evidence is | What a consumer should do | |---|---|---|---| @@ -680,17 +1031,20 @@ Each statement's impact string records *how* the patch is persisted — one mark | `Patched via Socket patch (vendored)` | vendored | The **committed** `.socket/vendor/` artifact was hash-verified — no install hook needed; the lockfile wiring is the persistence mechanism | Trust it on any checkout; the committed bytes are the patch | | `Patched via Socket patch (redirected)` | hosted | The lockfile's integrity pin points at the Socket-hosted patched package. When emitted in-run by `scan --mode hosted --vex`, the statement is attested **from the redirect ledger without hash verification** (the bytes are fetched at install time — the JSON `vex` summary carries `verified: false`) | Ensure installs still resolve from `patch.socket.dev` (the lockfile edit is intact), and run `socket-patch vex` **after installing** — it re-reads the ledger and hash-verifies the redirected patches against the installed tree | -The markers are stable strings (see `CLI_CONTRACT.md`); scanners and policy engines may match on them. +The markers are stable strings (see +[CLI_CONTRACT.md](crates/socket-patch-cli/CLI_CONTRACT.md)); scanners and policy engines +may match on them. **Output channels** -| Invocation | VEX document | stdout | -|------------|--------------|--------| -| _default_ (no `--output`, no `--json`) | stdout | human-readable status on stderr | -| `--output ` | the file | one-line summary | -| `--json --output ` | the file | machine-readable envelope (the CI shape) | +| Invocation | VEX document | Status / summary | +|------------|--------------|------------------| +| _default_ (no `--output`, no `--json`) | stdout | one-line summary (stderr) | +| `--output ` | the file | one-line summary (stdout) | +| `--json --output ` | the file | machine-readable envelope on stdout (the CI shape) | -`--json` requires `--output`, since the VEX document is itself JSON and would otherwise collide with the envelope on stdout. +`--json` requires `--output`, since the VEX document is itself JSON and would otherwise +collide with the envelope on stdout. **Using it with a scanner** @@ -705,66 +1059,82 @@ grype --vex socket.vex.json trivy image --vex socket.vex.json ``` -Run `socket-patch get` or `socket-patch scan --mode agent --prune` first — `vex` errors with `no_patches` when there is nothing to attest (an empty manifest and no detached-vendored patches). +Apply patches first (in any mode) — `vex` errors with `no_patches` when there is nothing +to attest (an empty manifest, no detached-vendored patches, and no hosted redirect +records). ### Inline VEX on `apply` / `scan` / `vendor` -You don't need a separate `vex` invocation: pass `--vex ` to `apply`, `scan`, or `vendor` and the same OpenVEX document is generated as a side-effect of a successful run. +You don't need a separate `vex` invocation: pass `--vex ` to `apply`, `scan`, or +`vendor` and the same OpenVEX document is generated as a side-effect of a successful run. ```bash # Patch and attest in one step socket-patch apply --vex socket.vex.json -# Discover, apply, prune, and attest — the full bot-mode pass +# Discover, apply, prune, and attest — the full auto-update-bot pass socket-patch scan --json --mode agent --prune --yes --vex socket.vex.json # Vendor and attest — works manifest-less with --detached too socket-patch scan --json --mode vendored --yes --vex socket.vex.json ``` -The `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, and `--vex-compact` flags mirror the standalone command's `--product` / `--no-verify` / `--doc-id` / `--compact` knobs. +The `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, and `--vex-compact` flags mirror +the standalone command's `--product` / `--no-verify` / `--doc-id` / `--compact` knobs. Contract: -- The document is **always written to the file** (never stdout), so it never collides with the command's own `--json` output. JSON mode adds a top-level `vex` summary — `{ path, statements, format }` — to the envelope (`apply`) / result (`scan`). -- It's built from the manifest **as it stands after the run** (including any `--mode agent` writes, with or without `--prune`) and verified against on-disk state unless `--vex-no-verify` is set. Generated for real applies, `--dry-run`, and read-only scans alike. -- **Fail-the-command:** if `--vex` was requested but generation fails (no detectable product, empty/missing manifest, nothing verified, unwritable path), the command exits non-zero **even when the apply/scan itself succeeded**, with a stable error code in the JSON output. +- The document is **always written to the file** (never stdout), so it never collides + with the command's own `--json` output. JSON mode adds a top-level `vex` summary — + `{ path, statements, format }` — to the envelope (`apply`) / result (`scan`). +- It's built from the manifest **as it stands after the run** (including any + `--mode agent` writes, with or without `--prune`) and verified against on-disk state + unless `--vex-no-verify` is set. Generated for real applies, `--dry-run`, and read-only + scans alike. +- **Fail-the-command:** if `--vex` was requested but generation fails (no detectable + product, empty/missing manifest, nothing verified, unwritable path), the command exits + non-zero **even when the apply/scan itself succeeded**, with a stable error code in the + JSON output. ## Scripting & CI/CD -All commands support `--json` for machine-readable output. JSON responses always include a `"status"` field for easy error detection: +All commands support `--json` for machine-readable output. JSON responses always include +a `"status"` field for easy error detection. + +**Authentication in CI:** a runner has no `socket login` state — if your organization +has org-tier patches, provide the token as a CI secret via `SOCKET_API_TOKEN` (without +it, runs silently fall back to the anonymous public proxy and see free patches only, and +paid-tier blob downloads report `paidRequired`). To deliberately pin a run to the +anonymous free tier, set `SOCKET_NO_API_TOKEN=1`. See +[Configuration sources](#configuration-sources). ```bash # Check for available patches in CI (read-only) result=$(socket-patch scan --json --ecosystems npm) patches=$(echo "$result" | jq '.totalPatches') -# Auto-update bot mode: discover, apply, prune, sweep in one pass +# Auto-update bot: discover, apply, and garbage-collect in one pass socket-patch scan --json --mode agent --prune --yes | jq '{ - applied: [.apply.patches[] | select(.action == "added" or .action == "updated") | .purl], - pruned: .gc.prunedManifestEntries, - bytes_freed: .gc.bytesFreed + applied: [.apply.patches[]? | select(.action == "added" or .action == "updated") | .purl], + pruned: (.gc.prunedManifestEntries // []), + bytes_freed: (.gc.bytesFreed // 0) }' -# Pipe this into peter-evans/create-pull-request to open a PR with the changes. +# The PR action (e.g. peter-evans/create-pull-request) commits the working-tree +# changes; use this summary as the PR body. # Apply patches and check result socket-patch apply --json | jq '.status' # "success", "partialFailure", "noManifest", or "error" ``` -When stdin is not a TTY (e.g., in CI pipelines), interactive prompts auto-proceed instead of blocking. Progress indicators and ANSI colors are automatically suppressed when output is piped. - -## Environment Variables +When stdin is not a TTY (e.g. in CI pipelines), interactive prompts auto-proceed instead +of blocking. Progress indicators and ANSI colors are automatically suppressed when output +is piped. -Every [Global Option](#global-options) has a matching `SOCKET_*` environment variable (listed in that table), and `vex`-specific flags map to `SOCKET_VEX_*`. The most commonly used variables are: +The exact JSON shapes, exit codes, and stability guarantees are specified in +[CLI_CONTRACT.md](crates/socket-patch-cli/CLI_CONTRACT.md). -| Variable | Description | -|----------|-------------| -| `SOCKET_API_TOKEN` | API authentication token. Use the raw token (`sktsec_<...>_api`) shown when it was generated, **not** the SHA-512 hash (`sha512-...`) that the dashboard may also display for identification. | -| `SOCKET_ORG_SLUG` | Default organization slug | -| `SOCKET_API_URL` | API base URL (default: `https://api.socket.dev`) | - -## Manifest Format +## Manifest format Downloaded patches are stored in `.socket/manifest.json`: @@ -796,13 +1166,22 @@ Downloaded patches are stored in `.socket/manifest.json`: } ``` -Patched file contents are in `.socket/blob/` (named by git SHA256 hash). - -## Supported Platforms - -| Platform | Architecture | -|----------|-------------| -| macOS | ARM64 (Apple Silicon), x86_64 (Intel) | -| Linux | x86_64, ARM64, ARMv7, i686 | -| Windows | x86_64, ARM64, i686 | -| Android | ARM64 | +Patched file contents are in `.socket/blobs/` (named by git SHA256 hash). + +The manifest may also carry an optional top-level `"setup"` key persisting setup state — +`"setup": { "manual": ["cargo"], "exclude": ["packages/legacy"] }` — where `manual` +lists ecosystems you patch by hand so [`vex`](#vex) still attests them (see +[`setup`](#setup)), and `exclude` lists workspace members excluded from setup (written +by `setup --exclude`). + +## Further reading + +- **[Ecosystem & platform support](docs/ecosystems.md)** — the full mode × ecosystem + matrix, per-ecosystem caveats (Maven, NuGet, Rush monorepos, Go), and supported + platforms. +- **[CLI contract](crates/socket-patch-cli/CLI_CONTRACT.md)** — the machine-readable + surface: exact JSON shapes, exit codes, flag/env bindings, and the semver policy that + governs them. +- **[Design notes](docs/design/)** — e.g. [the configuration model](docs/design/configuration.md) + and [why hosted mode is impossible for Go](docs/design/golang-hosted-no-go.md). +- **[Changelog](CHANGELOG.md)** diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index bb68e8e..0ee9cc4 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -387,10 +387,10 @@ per service outcome: Coverage today: **npm** (all lock flavors), **pypi** (wheel — sdist falls back / refuses), **cargo** (download + extract the `.crate`), **golang** (download + extract the module zip, verify the `h1:` dirhash, wire the `replace`), **composer** (download + extract the dist zip), **gem** (download + -extract the `.gem`, plus a `gem-stub-gemspec` SECOND artifact), and **nuget** (download the prebuilt -`.nupkg`). **maven** attempts the prebuilt `.jar` download under `auto` but is NOT in the fail-closed -`service` coverage list — `--vendor-source service` refuses maven purls with -`vendor_service_unsupported_ecosystem`. The Tier-B ecosystems +extract the `.gem`, plus a `gem-stub-gemspec` SECOND artifact), **nuget** (download the prebuilt +`.nupkg`), and **maven** (download the prebuilt `.jar` + the registry pom; in the fail-closed +`service` coverage list since the `service_mode_gate_admits_maven` fix — PR #117 shipped the backend +but left maven off `SERVICE_ECOSYSTEMS`). The Tier-B ecosystems (cargo/golang/composer/gem) download the patched archive and extract it into the vendor directory — the same source tree the local build commits — then run the existing path-dep wiring; their build-equivalence is exercised by the toolchain-backed e2e suites (which skip when the package diff --git a/docs/ecosystems.md b/docs/ecosystems.md new file mode 100644 index 0000000..f9e1599 --- /dev/null +++ b/docs/ecosystems.md @@ -0,0 +1,160 @@ +# Ecosystem & platform support + +This is the detailed support matrix for `socket-patch`: which package ecosystems work +with which [patch mode](../README.md#three-patch-modes), the per-ecosystem caveats, and +the platforms the binary ships for. + +For what the three modes *are* and how to choose between them, see +[How Socket Patch works](../README.md#how-socket-patch-works) in the README. + +## Mode × ecosystem matrix + +The backticked slug in each row is the value `-e`/`--ecosystems` accepts (e.g. +`--ecosystems npm,pypi,golang`). + +| Ecosystem | agent (`--mode agent`) | vendored (`--mode vendored`) | hosted (`--mode hosted`) | +|-----------|------------------------|------------------------------|--------------------------| +| npm (`npm`) — pnpm / yarn / berry / bun | ✅ any install layout; `setup` postinstall hook | ✅ five lockfile flavors: package-lock, yarn classic, yarn berry (node-modules linker; PnP refused), pnpm v9, bun `bun.lock` (binary `bun.lockb` refused with a `--save-text-lockfile` pointer). Rush monorepos refused (`vendor_rush_unsupported`) — see [Rush notes](#npm-rush-monorepos) | ✅ package-lock / npm-shrinkwrap, pnpm-lock.yaml, yarn classic, yarn berry, bun — berry and bun carry constraints, see [npm hosted-mode notes](#npm-hosted-mode-notes) | +| PyPI (`pypi`) — uv / poetry / pdm / pipenv / pip | ✅ `.pth` startup hook via `setup` | ✅ five lockfile flavors: uv, poetry, pdm, pipenv (lock rewired, but pipenv doesn't hash-check file entries — `vendor_integrity_unverified` warning; the committed wheel bytes are the protection), and requirements.txt (consumed by pip or `uv pip`) | ✅ requirements.txt + uv.lock. **poetry / pdm / pipenv locks are not rewritten** — use vendored | +| Cargo (`cargo`) | ✅ in-place + `.cargo-checksum.json` rewrite (shared registry-cache caveat — see [Cargo: shared registry cache](#cargo-shared-registry-cache)) | ✅ `[patch.crates-io]` path entry | ✅ per-patch sparse registry (`[registries.socket-patch-]` + Cargo.lock source/checksum) | +| RubyGems (`gem`) | ✅ Bundler plugin via `setup` | ✅ Gemfile + Gemfile.lock path pair | ✅ per-dep `source` block; the `CHECKSUMS` pin needs bundler ≥ 2.6 (older locks get a `redirect_gem_no_checksums_section` warning) | +| Go (`golang`) | ✅ `go.mod` `replace` → `.socket/go-patches/` — see [Go: directory replaces and go.sum](#go-directory-replaces-and-gosum) | ✅ `replace` → the committed vendor tree | ❌ **not possible** — sumdb, module-path identity, and default-GOPROXY leakage each rule it out; see [golang-hosted-no-go.md](design/golang-hosted-no-go.md). **Use vendored** (`redirect_golang_unsupported` names the remedy) | +| Maven (`maven`) | ⚠️ experimental, apply-only (no `setup` hook — reports `no_files`) — gated behind `SOCKET_EXPERIMENTAL_MAVEN=1` (in-place jar patching corrupts the `~/.m2` checksum sidecars); prefer vendored / hosted | ✅ committed maven2 `file://` repository. A root pom declaring `` (multi-module aggregator) is refused (`vendor_maven_multimodule_unsupported`), and a gradle-only project is refused (`vendor_gradle_unsupported`) | ✅ **pom projects only, fail-closed** — the patched jar is pinned at a Socket-only `-socket.` suffix; `${property}` versions are refused; Gradle gets a manual `exclusiveContent` snippet — see [Maven & NuGet caveats](#maven--nuget-caveats) | +| NuGet (`nuget`) | ⚠️ experimental, apply-only (no `setup` hook — reports `no_files`) — gated behind `SOCKET_EXPERIMENTAL_NUGET=1` (in-place patching breaks the `.nupkg.sha512` tamper-evidence sidecar); prefer vendored / hosted | ✅ committed folder feed + `packageSourceMapping` + `packages.lock.json` contentHash pin | ✅ `nuget.config` source + source-mapping, `packages.lock.json` contentHash rewrite. See the locked-mode note in [Maven & NuGet caveats](#maven--nuget-caveats) | +| Composer (`composer`) | ✅ post-install script events | ✅ `composer.lock` `dist: path` rewrite | ✅ `composer.lock` dist url + shasum rewrite | +| Deno (`deno`) | ✅ apply-only — no install hook (`setup` reports `no_files`); declare in `setup.manual` for VEX coverage | ❌ refused (`vendor_unsupported_ecosystem`) | ❌ not supported | + +> **Maven / NuGet discovery gate**: discovering *installed* Maven and NuGet packages (the +> crawl behind `scan` / `apply` / `vendor`) currently requires the same +> `SOCKET_EXPERIMENTAL_MAVEN=1` / `SOCKET_EXPERIMENTAL_NUGET=1` opt-in in every mode. The +> vendored/hosted wiring itself is safe — the gate guards the agent-mode sidecar risk. + +## npm hosted-mode notes + +- **yarn berry** — the redirect edits the `yarn.lock` entry only (cacheKey `10c0` / + yarn 4), and `.yarnrc.yml`'s `compressionLevel` must stay 0. The node-modules linker + is e2e-covered; PnP is untested for hosted — the lock rewrite fires, but PnP's + `.yarn/cache` resolution isn't exercised. +- **bun** — text `bun.lock` v1 only. A binary `bun.lockb` with no text lock beside it + is auto-migrated first: the CLI runs your installed `bun` + (`bun install --save-text-lockfile --frozen-lockfile --lockfile-only`) before reading + the lock — `redirect_bun_lockb_would_migrate` on `--dry-run`, + `redirect_bun_lockb_unsupported` when `bun` is unavailable. (Contrast vendored mode, + which refuses `bun.lockb` and leaves you to run the migration yourself.) + +## npm: Rush monorepos + +A Rush repo has no root `package.json`/lockfile pair — its pnpm source-of-truth locks +live at `common/config/rush/pnpm-lock.yaml` (plus one per subspace under +`common/config/subspaces//`). + +- **Hosted** ✅ — `scan --mode hosted` discovers and repoints those locks in place + (subspaces included). +- **Agent** ✅ — works through the generated project symlink farm. +- **Vendored** ❌ — refused (`vendor_rush_unsupported`): `rush install` copies the lock + into `common/temp` and runs pnpm there, so vendor's relative `file:` specs can't + survive the copy — the refusal routes you to hosted mode. + +Editing a Rush lock outside `rush update` desyncs the `pnpmShrinkwrapHash` in +`common/config/rush/repo-state.json`, so when `preventManualShrinkwrapChanges` is enabled +`rush install` fails until `rush update` refreshes it (a `redirect_rush_repo_state_stale` +warning flags this; the redirect survives the refresh — pnpm keeps locked resolutions for +unchanged specifiers). + +## Maven & NuGet caveats + +Honest limits of the Maven and NuGet flows — documented behavior, not bugs: + +* **Fail-closed by version suffixing (hosted Maven).** Maven has no lockfile, so hosted + mode pins the patch a different way: the Socket patch server (`patch.socket.dev`) + exposes the patched jar + under a globally-unique `-socket.` suffix that exists **only** on the + injected `socket-patch-` repository. The rewriter pins that suffixed version + explicitly — it rewrites the literal ``, or (for a transitive / managed + dependency with no literal version in your pom) adds a `` entry — + so a resolver that can't reach the Socket repo, or is handed different bytes, has + nowhere to fall through to: the build **hard-fails** instead of silently resolving the + unpatched upstream artifact. The ``'s `checksumPolicy=fail` still verifies + the transport-level `.jar.sha1` sidecar on top. A `${property}` version is refused + (`redirect_maven_dep_unpinned`) — a literal edit would break the property reference and + a depMgmt pin could strand sibling artifacts sharing the property. A literal version + that matches neither the base nor the suffixed value is skipped + (`redirect_maven_dep_version_mismatch`). +* **Trusted Checksums reinforcement (hosted Maven, 3.9+).** When the patch server + supplies both the jar and pom sha256, the rewriter also emits Maven + [Trusted Checksums](https://maven.apache.org/resolver/expected-checksums.html) files — + `.mvn/maven.config` resolver args plus `.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 and + surfaces `redirect_maven_trusted_checksums_conflict`). This is an **independent + client-side content pin** on top of the transport check. It requires **Maven 3.9+** + (the resolver post-processor and the `${session.rootDirectory}` basedir expression the + config uses); on older Maven the `.mvn/*` files are silently inert — the + version-suffixing above is still fail-closed on its own. On Maven **3.9.0–3.9.8** a + *mismatch* is enforced but reported unclearly; the readability fix landed in **3.9.9** + ([MNG-8182](https://issues.apache.org/jira/browse/MNG-8182)). The args are + `originAware=false` and `failIfMissing=false`, so one checksum matches the artifact + from any repository and a dependency with no committed checksum still resolves — only a + *mismatch* fails. +* **Warm `~/.m2` shadowing (vendored Maven only).** Maven consults the *local repository* + before any configured ``, so with vendored mode a warm `~/.m2` copy of the + same GAV silently wins over the committed `file://` repository — the build succeeds + with **unpatched** bytes. Purge it with: + `mvn dependency:purge-local-repository -DmanualInclude=:` + (the always-on `vendor_maven_local_cache_shadow` warning carries the same one-liner). + Hosted mode is **not** affected: the patched jar lives at the suffixed version, which + no warm `~/.m2` entry can hold. +* **`mirrorOf` mirrors (hosted Maven).** A `settings.xml` `` with + `*` (common in corporate environments) reroutes *all* repositories + — including the injected `socket-patch-` repository — through the mirror. Because + the patch resolves only at the suffixed version, the mirror (which does not carry it) + can't serve it and the **build fails loudly** rather than silently going unpatched. + Scope the mirror to exclude the Socket repos (e.g. + `*,!socket-patch-*`) so the redirect resolves; the + `originAware=false` Trusted Checksums act as a backstop when present. +* **Gradle (hosted Maven).** Gradle build scripts are never edited. A present + `build.gradle*` / `settings.gradle*` gets a paste-able `exclusiveContent { … }` snippet + (a `redirect_gradle_manual_snippet` warning) that carries the **suffixed** version — + and you must bump the `groupId:artifactId` dependency declaration to that suffixed + version yourself. It is fail-closed by repository exclusivity: the `exclusiveContent` + filter routes only the suffixed version to the Socket repo, which is the only place it + exists. +* **NuGet locked mode (hosted + vendored).** With a `packages.lock.json` and + `dotnet restore --locked-mode`, the rewritten `contentHash` pins the patched `.nupkg` — + a tampered or wrong package fails restore with `NU1403`. Without a lockfile there is no + client-side content pin (vendored surfaces this as a `vendor_nuget_no_lockfile` + warning; the feed + source mapping still force the patched copy). + +## Cargo: shared registry cache + +Agent mode patches the crate in place wherever the crawler finds it. For a non-vendored +crate that means the **shared** `$CARGO_HOME/registry` cache: the patch affects every +project on the machine, and is silently reset by `cargo clean` or a cache prune. Use +`--mode vendored` for a project-local, committable patch. + +## Go: directory replaces and go.sum + +Both Go modes work through a `go.mod` `replace` directive pointing at a committed +directory — `.socket/go-patches/@/` in agent mode, +`.socket/vendor/golang//@/` in vendored mode — because the module +cache is `go.sum`-verified, so patching it in place can't build. Go **never verifies a +directory `replace` target against `go.sum`** — that is by design (it's how local module +development works), and it means the committed patched tree itself is the protection: +commit it, and review it like any other vendored code. The wiring survives +`go mod tidy`, and `apply --check` gives CI a read-only audit that the committed +redirects still match the manifest. + +Hosted mode is a hard ❌ for Go — sumdb verification, module-path identity, and +default-GOPROXY leakage each independently rule it out; the full analysis is in +[golang-hosted-no-go.md](design/golang-hosted-no-go.md). + +## Supported platforms + +Prebuilt binaries are published for: + +| Platform | Architecture | +|----------|-------------| +| macOS | ARM64 (Apple Silicon), x86_64 (Intel) | +| Linux | x86_64, ARM64, 32-bit ARM hard-float (`arm-unknown-linux-gnueabihf` / `-musleabihf`), i686 | +| Windows | x86_64, ARM64, i686 | +| Android | ARM64 |