Skip to content

fix(cli)!: require consent before patching user-patched packages - #1646

Open
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/respect-user-package-patches
Open

Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/respect-user-package-patches

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

A project could already have a pnpm, Yarn or patch-package fix and still receive a Socket patch without specific consent. Native npm/Bun had backend-specific refusals, but there was no shared package-ownership check. Agent apply also overwrote unknown installed file contents with the verified Socket blob by default, potentially discarding a local fix.

This change protects project patches before patch selection or mutation, even when the two fixes touch different files or the user diff has not been installed yet.

  • Add a read-only detector for pnpm declarations and lock markers; Yarn user and plugin patch locators; patch-package filenames and literal custom directories; native npm/Bun declarations; Composer patch definitions and locks; Cargo overrides; and Go replacements. Inspect declared npm workspace members and ancestor workspace settings. Preserve Socket-owned wiring and distinguish Yarn's builtin compatibility patches from user patches.
  • Skip protected scan candidates with user_patch_conflict warnings in every mode. Explicit get, apply, and vendor refuse conflicts with exit 1. Recheck ownership before hosted/vendored writes, including hosted-to-vendored eject, and retain the full crawl for pruning.
  • Add invocation-only --allow-user-patches, forwarded through nested apply and reported as user_patch_override. General --yes, UUID selection, policy settings, and --force do not approve declared patch composition. Existing backend safety refusals remain in force.
  • Reject unknown installed-file drift by default. Explicit consent restores the previous hash-verified overwrite behavior; --strict keeps rejecting drift even with consent. Existing apply --force remains an explicit override for file drift only. Consent is an attempt to combine changes, not an automatic merge.

The CLI contract and usage docs specify diagnostics, read-only/reversal exemptions, version matching, workspace behavior, and detection limits. The ecosystem documentation links the primary package-manager documentation and source used for the research. Regression coverage includes pnpm wildcard prereleases, nested Yarn patch locators in both directions, scoped/nested/sequenced patch-package names, malformed metadata, all CLI modes, nested opt-in forwarding, and preservation of user files.

Breaking change / release coordination: the stricter mismatch default and package eligibility belong in the next major release under the CLI semver policy. Version stamps and release notes remain the release train's responsibility.

Detection is conservative where metadata is unreadable or cannot be resolved without executing scripts. Arbitrary build hooks, external patch pipelines, global-install declarations, and unrecorded edits to files a Socket patch does not touch cannot be detected reliably. A nested agent apply also checks existing in-scope manifest entries; it can refuse an older protected entry after newly selected records have been saved. These limits are explicit in the contract.

Validation passed locally:

  • cargo check --locked -p socket-patch-cli --tests
  • Core detector tests: 12 passed; CLI unit tests: 925 passed.
  • Full apply, get, scan, vendor, hosted eject, in-process vendor, user-patch, and affected global/parser integration suites.
  • cargo clippy --locked --workspace --all-features -- -D warnings
  • cargo fmt --all -- --check and git diff --check
  • A real pnpm 7.28.0 patch / patch-commit workflow with a local Socket test record targeting a different file: default apply and --force refused; --allow-user-patches applied the Socket fix while preserving the native diff and user-modified file.

Draft until the repository-required /code-review high workflow has run. That workflow is not available in the authoring session; the implementation has been manually reviewed and validated as above.


Note

High Risk
Major behavior change to default apply eligibility and file-mismatch policy on mutating CLI paths; incorrect detection could block or overwrite user-owned dependency changes.

Overview
Breaking (major): Socket Patch no longer applies on top of declared project patches or overwrites locally modified dependency files unless the user opts in.

Adds --allow-user-patches (invocation-only; not env/policy) and a core UserPatchIndex that scans lockfiles and manifests for pnpm/Yarn/patch-package/npm/Bun/Composer/Cargo/Go patch evidence (plus workspaces). scan drops protected packages with user_patch_conflict warnings; get, apply, vendor, and hosted/vendored write paths exit with user_patch_conflict unless consent is given, emitting user_patch_override when allowed. --force still overrides file-hash drift only; declared patches still need --allow-user-patches.

Default file mismatch behavior flips: unknown content (neither beforeHash nor afterHash) now fails closed; warn-and-overwrite with verified afterHash content requires --allow-user-patches. --strict continues to reject drift even with that flag.

CLI contract, usage, and ecosystem docs document detection scope, diagnostics, and limits.

Reviewed by Cursor Bugbot for commit 98d2edd. Configure here.


Generated by Claude Code

Detect project patch ownership across pnpm, Yarn, patch-package, npm, Bun, Composer, Cargo and Go. Skip protected scan candidates and refuse explicit mutations unless --allow-user-patches is passed. Preserve native backend refusals and require explicit consent before overwriting unknown installed file contents.

BREAKING CHANGE: declared project patches are protected by default and agent apply now rejects unknown file-content drift. Document the next-major-release requirement, detection limits and invocation-only consent in the CLI contract.
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 20, 2026 22:24

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but none of the reported issues met your autofix severity threshold.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 98d2edd. Configure here.

Err(error) => {
let code = report_apply_failure(&args, &error, &error.message, &telemetry).await;
return ApplyRunReport::run_failure(code, &error.code, error.message);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply holds lock during conflict telemetry

Low Severity

A user_patch_conflict in run_locked reports the failure and sends telemetry without releasing apply.lock first. The two manifest-load failures immediately above call lock.release() before report_apply_failure, because that helper can POST. Concurrent apply, scan, vendor, or repair stay blocked until the report finishes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 98d2edd. Configure here.

e.name.eq_ignore_ascii_case(name)
} else {
e.name == name
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cargo patch names match case-sensitively

Medium Severity

conflicts treats only Composer names as case-insensitive. Cargo crate names are case-insensitive, so a [patch] or [replace] key that differs in case from the Socket PURL does not match. That package stays eligible and can be patched without --allow-user-patches.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 98d2edd. Configure here.

.filter(|g| !g.starts_with('!') || g.starts_with("!("))
{
pending.extend(index.workspace_members(&dir, pattern));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go workspace members are not inspected

Medium Severity

Discovery reads replace directives only in each queued directory’s go.mod / go.work. It never walks go.work use members, unlike npm workspace globs. A member-only replace is invisible from the workspace root, so that module can receive a Socket patch without consent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 98d2edd. Configure here.

silent,
all_releases: args.all_releases,
strict: args.common.strict,
allow_user_patches: args.common.allow_user_patches,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scan warnings ignore silent mode

Low Severity

Scan pushes user_patch_conflict and user_patch_override through print_layout_refusals, which prints Warning: even when --silent is set. The dedicated check helper already suppresses those warnings under --silent. Human scan output therefore leaks skip/override details that silent mode should hide.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: CLI warnings must be surfaced in both stderr (human) and JSON envelope (machine)

Reviewed by Cursor Bugbot for commit 98d2edd. Configure here.

The url-spelled crates-io [patch] table and ancestor .cargo/config
[patch] fixtures are now caught earlier by the new UserPatchIndex,
which refuses with user_patch_conflict before the cargo-specific
guards run. Pass --allow-user-patches so these tests keep exercising
the guards they were written for (cargo_manifest_patch_source_alias
and user_authored_patch_entry); the default refusal is covered by
tests/user_patches.rs.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: CI on 98d2edd had two separate failures.

Fixed (29dd054): e2e_vendor_cargo_build cargo_url_spelled_crates_io_patch_table_is_refused and cargo_ancestor_config_patch_cannot_silently_shadow_the_vendored_copy. The new UserPatchIndex now refuses these fixtures with user_patch_conflict before the cargo-specific guards run. The tests now pass --allow-user-patches, so they still exercise cargo_manifest_patch_source_alias / user_authored_patch_entry. Both pass locally.

Needs a decision (coverage job): 5 tests in tests/in_process_redirect_pnpm.rs fail: hosted_scan_from_package_json_workspace_member_refuses, hosted_scan_from_npm_member_with_stray_lock_refuses, hosted_scan_from_bun_or_vlt_member_with_stray_lock_refuses, hosted_scan_from_brace_or_class_glob_workspace_member_refuses, and hosted_run_from_berry_picomatch_workspace_member_refuses. No user patch is declared in any of these fixtures. The detector fails closed anyway:

  • package-lock.json / bun.lock (cannot inspect patch metadata: expected value at line 1 column 1): the fixture lock is a non-JSON placeholder.
  • (workspace outside the inspection root): the berry packages/b/../a glob.

scan --mode hosted then drops the package with a user_patch_conflict warning and exits 0 / status success. Before this PR it exited 1 / status error, and the tests assert exactly that ("a found-but-unpinnable patch is not success").

Question: when the protection comes only from an inspection failure (unparseable lockfile, workspace glob outside the root) and not from real patch evidence, should scan still exit 0 and report success? Or should that case stay an error (exit 1)? If exit 0 is intended, the five tests need new expectations. If not, scan (or UserPatchIndex) needs to tell inspection failures apart from declared patches. I didn't pick one, because either choice changes the PR's breaking-change contract.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants