From b573b8ddc16b37d6c24d7c572ba0f71e9323c3b1 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 09:24:40 -0400 Subject: [PATCH 1/3] Remove the SOCKET_FORCE env binding; --force is flag-only (#615) `apply --force`, `vendor --force` and `--update --force` all bound the same SOCKET_FORCE variable, so exporting it for one command quietly weakened checks in the others. Nothing sets it: no hook, wrapper, installer, workflow or other SocketDev repo. Drop the env binding from all three flags and from LOCAL_ARG_ENV_VARS. The variable is now ignored without a warning, like the other env vars v5 retires; a stale export leaves the beforeHash check and the managed-install refusal on. Replace the vendor env-wiring tests with one that pins SOCKET_FORCE as ignored, and add an args.rs regression test covering apply, vendor, self-update and --update (env ignored, --force still works). Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-cli/src/args.rs | 46 ++++++++++++++++-- crates/socket-patch-cli/src/commands/apply.rs | 1 - .../socket-patch-cli/src/commands/update.rs | 1 - .../socket-patch-cli/src/commands/vendor.rs | 1 - .../tests/cli_parse_vendor.rs | 47 +++++-------------- .../socket-patch-cli/tests/cli_parse_vex.rs | 2 +- .../tests/e2e_embedded_vex.rs | 2 +- 7 files changed, 56 insertions(+), 44 deletions(-) diff --git a/crates/socket-patch-cli/src/args.rs b/crates/socket-patch-cli/src/args.rs index a2ecfe542..014f8790b 100644 --- a/crates/socket-patch-cli/src/args.rs +++ b/crates/socket-patch-cli/src/args.rs @@ -613,7 +613,6 @@ pub const GLOBAL_ARG_ENV_VARS: &[&str] = &[ /// missing here escapes the empty-var scrub — the invariant tests below /// parse every entry against its owning subcommand to keep this honest. pub const LOCAL_ARG_ENV_VARS: &[&str] = &[ - "SOCKET_FORCE", "SOCKET_PATCH_VERSION", "SOCKET_SAVE_ONLY", "SOCKET_ALL_RELEASES", @@ -1560,9 +1559,6 @@ mod tests { // (env var, argv of a subcommand that binds it) — every bool entry // of `LOCAL_ARG_ENV_VARS`, on each subcommand that binds it. const BOOL_BINDINGS: &[(&str, &[&str])] = &[ - ("SOCKET_FORCE", &["socket-patch", "apply"]), - ("SOCKET_FORCE", &["socket-patch", "vendor"]), - ("SOCKET_FORCE", &["socket-patch", "self-update"]), ("SOCKET_SAVE_ONLY", &["socket-patch", "get", "x"]), ("SOCKET_ALL_RELEASES", &["socket-patch", "get", "x"]), ("SOCKET_ALL_RELEASES", &["socket-patch", "scan"]), @@ -1600,6 +1596,48 @@ mod tests { }); } + /// v5 retired `SOCKET_FORCE` (#615): `--force` is flag-only on `apply`, + /// `vendor` and `--update`. A stale export must be ignored, so it can + /// never quietly skip the beforeHash check or the managed-install + /// refusal; the flag itself still works. + #[test] + #[serial_test::serial] + fn socket_force_env_is_ignored_and_force_flag_still_works() { + fn force_of(argv: &[&str]) -> bool { + let argv = argv.iter().map(|s| s.to_string()).collect(); + match crate::parse_argv_with_shortcuts(argv) + .unwrap_or_else(|e| panic!("parse failed: {e}")) + .command + { + crate::Commands::Apply(a) => a.force, + crate::Commands::Vendor(a) => a.force, + crate::Commands::SelfUpdate(a) => a.force, + _ => panic!("unexpected subcommand"), + } + } + + const ARGVS: &[&[&str]] = &[ + &["socket-patch", "apply"], + &["socket-patch", "vendor"], + &["socket-patch", "self-update"], + &["socket-patch", "--update"], + ]; + + with_clean_socket_env(|| { + with_env_cleared(&["SOCKET_FORCE"], || { + for val in ["1", "true", "yes"] { + std::env::set_var("SOCKET_FORCE", val); + for &argv in ARGVS { + assert!(!force_of(argv), "SOCKET_FORCE={val} set force on {argv:?}"); + let mut with_flag = argv.to_vec(); + with_flag.push("--force"); + assert!(force_of(&with_flag), "--force ignored on {argv:?}"); + } + } + }); + }); + } + /// Companion invariant for the **value-typed** local env vars: an /// exported-but-empty value (`VAR=`) must not crash its subcommand — /// [`scrub_empty_env_vars`] (run by `main` before clap) removes it, and diff --git a/crates/socket-patch-cli/src/commands/apply.rs b/crates/socket-patch-cli/src/commands/apply.rs index d3a23819f..6ddde1673 100644 --- a/crates/socket-patch-cli/src/commands/apply.rs +++ b/crates/socket-patch-cli/src/commands/apply.rs @@ -359,7 +359,6 @@ pub struct ApplyArgs { #[arg( short = 'f', long, - env = "SOCKET_FORCE", default_value_t = false, value_parser = crate::args::parse_bool_flag, )] diff --git a/crates/socket-patch-cli/src/commands/update.rs b/crates/socket-patch-cli/src/commands/update.rs index fea0d06a5..710431f86 100644 --- a/crates/socket-patch-cli/src/commands/update.rs +++ b/crates/socket-patch-cli/src/commands/update.rs @@ -58,7 +58,6 @@ pub struct UpdateArgs { /// on the requested version. #[arg( long, - env = "SOCKET_FORCE", default_value_t = false, value_parser = parse_bool_flag, )] diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index becee8673..7b448ff59 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -77,7 +77,6 @@ pub struct VendorArgs { #[arg( short = 'f', long, - env = "SOCKET_FORCE", default_value_t = false, value_parser = crate::args::parse_bool_flag, )] diff --git a/crates/socket-patch-cli/tests/cli_parse_vendor.rs b/crates/socket-patch-cli/tests/cli_parse_vendor.rs index 9106903cd..7208af9a0 100644 --- a/crates/socket-patch-cli/tests/cli_parse_vendor.rs +++ b/crates/socket-patch-cli/tests/cli_parse_vendor.rs @@ -2,7 +2,8 @@ //! //! These tests pin the public CLI contract for `socket-patch vendor`: every //! flag, every default, the embedded-VEX passthrough surface, env-var -//! wiring (`SOCKET_FORCE`, `SOCKET_VENDOR_REVERT`, `SOCKET_VEX*`), the +//! wiring (`SOCKET_VENDOR_REVERT`, `SOCKET_VEX*`; the retired `SOCKET_FORCE` +//! is pinned as ignored), the //! subcommand's presence in the top-level command list, and that the //! bare-UUID convenience fallback still routes to `get` — never to //! `vendor`. Changing any assertion here is a breaking change to the CLI @@ -61,7 +62,8 @@ const SOCKET_ENV_VARS: &[&str] = &[ "SOCKET_NO_TRUST_LOCKFILE_CONFIG", "SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG", "SOCKET_NO_VLT_INSTALL_CLEANUP", - // VendorArgs-specific + // VendorArgs-specific (`SOCKET_FORCE` is retired but still scrubbed so a + // stale export can't leak into the "is ignored" test) "SOCKET_FORCE", "SOCKET_VENDOR_REVERT", // VexEmbedArgs (flattened embedded-VEX passthrough) @@ -432,41 +434,16 @@ fn ecosystems_csv_splits_into_vec() { // injected variable, so the parsed value can only have come from that // variable (not from the shell, and not from a flag). +/// v5 retired `SOCKET_FORCE` (#615): `--force` is flag-only, so a stale +/// export leaves `force` at its default instead of bypassing the variant +/// probe. The var stays in the scrub list so this test controls it. #[test] #[serial_test::serial] -fn env_socket_force_true_sets_force() { - let a = parse_vendor_with_env(&[("SOCKET_FORCE", "true")], &[]).expect("parse"); - let mut want = expected_defaults(); - want.force = true; - assert_eq!(snapshot(&a), want); -} - -#[test] -#[serial_test::serial] -fn env_socket_force_false_keeps_force_off() { - let a = parse_vendor_with_env(&[("SOCKET_FORCE", "false")], &[]).expect("parse"); - assert_eq!(snapshot(&a), expected_defaults()); -} - -/// The contract every other bool env var on this CLI follows (`SOCKET_JSON=1`, -/// `SOCKET_OFFLINE=yes`, `SOCKET_VENDOR_REVERT=1` all work): boolish tokens -/// must be accepted. `SOCKET_FORCE=1` should set `force = true`. -#[test] -#[serial_test::serial] -fn env_socket_force_numeric_one_should_set_force() { - let a = parse_vendor_with_env(&[("SOCKET_FORCE", "1")], &[]) - .expect("boolish env tokens should be accepted like every other SOCKET_* bool"); - let mut want = expected_defaults(); - want.force = true; - assert_eq!(snapshot(&a), want); -} - -#[test] -#[serial_test::serial] -fn env_socket_force_empty_should_parse_as_false() { - let a = parse_vendor_with_env(&[("SOCKET_FORCE", "")], &[]) - .expect("an exported-but-empty bool env var must not abort the parse"); - assert_eq!(snapshot(&a), expected_defaults()); +fn env_socket_force_is_ignored() { + for val in ["1", "true", "yes", ""] { + let a = parse_vendor_with_env(&[("SOCKET_FORCE", val)], &[]).expect("parse"); + assert_eq!(snapshot(&a), expected_defaults(), "SOCKET_FORCE={val:?}"); + } } #[test] diff --git a/crates/socket-patch-cli/tests/cli_parse_vex.rs b/crates/socket-patch-cli/tests/cli_parse_vex.rs index 9f3694d9a..25e184bcd 100644 --- a/crates/socket-patch-cli/tests/cli_parse_vex.rs +++ b/crates/socket-patch-cli/tests/cli_parse_vex.rs @@ -67,7 +67,7 @@ const SOCKET_ENV_VARS: &[&str] = &[ "SOCKET_VEX_NO_VERIFY", "SOCKET_VEX_DOC_ID", "SOCKET_VEX_COMPACT", - // ApplyArgs-specific + // Retired in v5 (#615); still scrubbed for hermeticity "SOCKET_FORCE", // ScanArgs-specific "SOCKET_BATCH_SIZE", diff --git a/crates/socket-patch-cli/tests/e2e_embedded_vex.rs b/crates/socket-patch-cli/tests/e2e_embedded_vex.rs index 80ecc55d3..2d2da4261 100644 --- a/crates/socket-patch-cli/tests/e2e_embedded_vex.rs +++ b/crates/socket-patch-cli/tests/e2e_embedded_vex.rs @@ -30,7 +30,7 @@ fn binary() -> &'static str { /// Every embedded-VEX flag has an env fallback (`--vex`/`SOCKET_VEX`, /// `--vex-product`/`SOCKET_VEX_PRODUCT`, `--vex-no-verify`/ /// `SOCKET_VEX_NO_VERIFY`, `--vex-doc-id`, `--vex-compact`), as do the -/// `GlobalArgs` (`SOCKET_OFFLINE`, `SOCKET_FORCE`, `SOCKET_API_TOKEN`, +/// `GlobalArgs` (`SOCKET_OFFLINE`, `SOCKET_API_TOKEN`, /// `SOCKET_ORG_SLUG`, …). If the ambient environment leaks any of these into /// the child, a test silently stops exercising the path it names — /// `apply_vex_failure_flips_exit_code` would no longer hit From 92e91618f3e8182465b9e9dba23c312bb6921a66 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 09:24:49 -0400 Subject: [PATCH 2/3] Document SOCKET_FORCE removal in the contract and v5 migration guide (#615) Set the env column of the apply/vendor --force rows to none, drop the env note from `--update --force` and the env-var table row, and list SOCKET_FORCE under the contract's removed env vars and the migration guide's retired spellings. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-cli/CLI_CONTRACT.md | 9 +++++---- docs/migrating-to-v5.md | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 7eddb127c..5c2cedb9b 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -84,9 +84,9 @@ Beyond the globals above, each subcommand defines a small set of local arguments | Subcommand | Local arg | Env var | Purpose | |---|---|---|---| -| `apply` | `--force` / `-f` | `SOCKET_FORCE` | Bypass beforeHash check | +| `apply` | `--force` / `-f` | — | Bypass beforeHash check | | `apply` | `--check` | — | Read-only audit that the committed **Go** `replace`-redirects match the manifest (CI / GitHub-App auditing) — Go ONLY (cargo patches in place, so there is no redirect to audit). Lock-free, crawl-free, offline-safe; exits 0 in sync, 1 on drift. Vendored modules are excluded from the audit | -| `vendor` | `--force` / `-f` | `SOCKET_FORCE` | Tolerate missing patch-target files in the stage + bypass the variant probe. A beforeHash mismatch no longer needs it: vendor staging auto-overwrites with the verified patched content (`vendor_content_mismatch_overwritten` warning) | +| `vendor` | `--force` / `-f` | — | Tolerate missing patch-target files in the stage + bypass the variant probe. A beforeHash mismatch no longer needs it: vendor staging auto-overwrites with the verified patched content (`vendor_content_mismatch_overwritten` warning) | | `vendor` | `--revert` | `SOCKET_VENDOR_REVERT` | Undo vendoring: restore recorded original lockfile fragments + remove `.socket/vendor/` artifacts. Works without a manifest. A package vendored over a hosted pin returns to its upstream registry entry, never to hosted (see "Takeover reconciliation") | | `vendor` | `--check` | — | Offline, read-only artifact and wiring audit; exits 1 on drift. Conflicts with `--revert`. | | `vendor` | `--local-repo ` | — | With `--check`, also inspect suffixed Maven jar/POM copies in this cache for conflicts. | @@ -960,7 +960,7 @@ Synopsis and behavior: |---|---| | `--update` | Resolve the latest release; install it if newer than the running version. Already-newest (including a dev build newer than any release): informational no-op, exit 0. `latest` never downgrades. | | `--update 3.4.0` | Install exactly that version, **up or down** — an explicit pin is explicit intent, no `--force` needed. Pin == current: no-op, exit 0. The inline `--update=3.4.0` spelling is equivalent. Also settable via `SOCKET_PATCH_VERSION` (the same pin env `install.sh` honors); a malformed version is a usage error (exit 2). | -| `--update --force` | Reinstall/downgrade even when already at the target version, and proceed past a managed-install refusal (with a warning that the owning manager's next upgrade will overwrite the binary). Env: `SOCKET_FORCE`. | +| `--update --force` | Reinstall/downgrade even when already at the target version, and proceed past a managed-install refusal (with a warning that the owning manager's next upgrade will overwrite the binary). Flag only (no env var). | | `--update --dry-run` | **Check-only**: one metadata request, zero downloads, zero mutation, exit 0 — and always the `verified`/`update_check` event shape, whether or not an update exists. `--json` details carry `{current, latest, updateAvailable, target, asset, path}` — the cheap scriptable "is an update available" probe. | | `--update --offline` | Refused up front (strict airgap, before any client exists), exit 1. `--force` does **not** bypass it. | @@ -1042,7 +1042,6 @@ Empty string means unset at every layer: exported-but-empty flag-bound vars are | `SOCKET_NO_TRUST_LOCKFILE_CONFIG` | `--no-trust-lockfile-config` | `false` | Hosted mode: skip the `trustLockfile: true` write to `pnpm-workspace.yaml`. | | `SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG` | `--no-npm-allow-remote-config` | `false` | Hosted mode: skip the `allow-remote=all` write to the project `.npmrc`. | | `SOCKET_NO_VLT_INSTALL_CLEANUP` | `--no-vlt-install-cleanup` | `false` | Hosted mode, `rollback`, `remove`: keep stale vlt installed copies. | -| `SOCKET_FORCE` | `apply --force` / `-f`, `vendor --force` / `-f`, `--update --force` | `false` | Local to `apply`, `vendor` and `--update`. | | `SOCKET_PATCH_VERSION` | `--update ` | (latest) | Local to `--update`; the same pin `install.sh` honors. | | `SOCKET_BATCH_SIZE` | `scan --batch-size` | `500` authenticated / `100` proxy | Local to `scan`. | | `SOCKET_MAX_NEW_PATCHES` | `scan --max-new-patches` | (unlimited) | Local to `scan` (v5.0): a count or `none`; empty is unset, malformed exits 2. | @@ -1127,6 +1126,8 @@ These exist for mirrors and testing. They are **internal**: names, semantics, an The v3.0 legacy names `SOCKET_PATCH_PROXY_URL`, `SOCKET_PATCH_DEBUG` and `SOCKET_PATCH_TELEMETRY_DISABLED` were removed in v5.0 and are ignored; use `SOCKET_PROXY_URL`, `SOCKET_DEBUG` and `SOCKET_TELEMETRY_DISABLED`. +`SOCKET_FORCE` was removed in v5.0 and is ignored without a warning (#615). `apply --force`, `vendor --force` and `--update --force` are flag-only: exporting the variable for one command no longer weakens the checks of the others. A stale export now leaves the beforeHash check and the managed-install refusal on. + ## CSV value parsing `--ecosystems` on `apply`, `rollback`, and `scan` uses clap's `value_delimiter = ','`. Input `--ecosystems npm,pypi,cargo` becomes `vec!["npm", "pypi", "cargo"]`. Switching to space-separated or dropping the delimiter is a **breaking** change. diff --git a/docs/migrating-to-v5.md b/docs/migrating-to-v5.md index e597b32d0..fda8e7f22 100644 --- a/docs/migrating-to-v5.md +++ b/docs/migrating-to-v5.md @@ -73,6 +73,7 @@ run `socket-patch apply` once after migration to confirm the manifest still appl | `SOCKET_PATCH_PROXY_URL` | `SOCKET_PROXY_URL` | | `SOCKET_PATCH_DEBUG` | `SOCKET_DEBUG` | | `SOCKET_PATCH_TELEMETRY_DISABLED` | `SOCKET_TELEMETRY_DISABLED` | +| `SOCKET_FORCE` | Pass `--force` to the one command that needs it (`apply`, `vendor`, `--update`); the variable is now ignored | Legacy `.socket/packages/` archives are no longer read. Patch data uses diff archives or blobs; cleanup commands remove obsolete package archives. From 436149f92606cc5151788dc7536c526117a80dc3 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Wed, 7 Oct 2026 09:59:47 -0400 Subject: [PATCH 3/3] Mention SOCKET_FORCE in the contract's env-var intro (#615) The intro still said only the three v3/v4 aliases were removed in v5, while the Removed env vars section it links to now also lists SOCKET_FORCE. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-cli/CLI_CONTRACT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 5c2cedb9b..c45a84b1f 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -1008,7 +1008,7 @@ State lives at `$XDG_CACHE_HOME`|`~/.cache` (Unix/macOS) or `%LOCALAPPDATA%` (Wi ## Environment variables -Public configuration uses the `SOCKET_*` names below. The three deprecated v3/v4 environment aliases were removed in v5; see [Removed env vars](#removed-env-vars). +Public configuration uses the `SOCKET_*` names below. The three deprecated v3/v4 environment aliases and `SOCKET_FORCE` were removed in v5; see [Removed env vars](#removed-env-vars). Four `SOCKET_CLI_*` names from the sibling JS Socket CLI are additionally accepted as **peer aliases** (supported, not deprecated — no warning): `SOCKET_CLI_API_TOKEN` → `SOCKET_API_TOKEN`, `SOCKET_CLI_ORG_SLUG` → `SOCKET_ORG_SLUG`, `SOCKET_CLI_API_BASE_URL` → `SOCKET_API_URL`, `SOCKET_CLI_NO_API_TOKEN` → `SOCKET_NO_API_TOKEN`. The canonical `SOCKET_*` name always wins when both are set; promotion is silent and happens in-process before clap parses. Other socket-cli names (`SOCKET_CLI_CONFIG`, `SOCKET_CLI_API_PROXY`, `SOCKET_CLI_DEBUG`) are deliberately **not** honored.