fix(gem): setup --remove clears bundler's machine-local .bundle/plugin registration - #210
Merged
Mikola Lysenko (mikolalysenko) merged 7 commits intoAug 19, 2026
Conversation
…egistration Pins the 2026-08 e2e campaign finding (D3/C3, P2): setup --remove unwires the Gemfile and deletes the generated plugin files, but leaves bundler's machine-local .bundle/plugin/index registration (hook subscriptions + plugin/load paths) dangling, so every later bundle install prints bundler's 5-line "plugin paths don't exist ... Continuing without installing plugin socket-patch" block with a misleading reinstall suggestion. Three failing guards: - host_guard::gem_setup_roundtrip_host now seeds the index exactly as bundler writes it and asserts remove leaves no socket-patch entry (index deleted when nothing else is registered); - host_guard::gem_setup_remove_strips_registration_surgically_under_ bundle_app_config: surgical strip that preserves another plugin's entries verbatim, drops emptied hook events, and follows bundler's BUNDLE_APP_CONFIG resolution (relative value resolves against the project root); - plugin_runtime::setup_remove_clears_bundler_plugin_registration: real host-bundler flow — install registers, remove must clear, and the next bundle install must print neither "plugin paths don't exist" nor "Continuing without installing plugin". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n registration Campaign finding D3/C3 (P2, bundler 2.7.2 + 4.0.18 repros): the unwire deleted the generated plugin files/stamp and restored the Gemfile, but never touched the .bundle/plugin/index registration (hook subscriptions and plugin/load paths) bundler wrote at the wired plugin's first install, so every later bundle install printed bundler's 5-line "plugin paths don't exist ... Continuing without installing plugin socket-patch" block with a misleading reinstall suggestion, forever. remove_plugin_directive now runs a third step after the Gemfile un-wire and plugin-file removal (never before — while the directive is wired the registration is live state, not residue): - resolves the index location exactly like Bundler.app_config_path: $BUNDLE_APP_CONFIG when set (relative values resolve against the project root — the official ruby images export BUNDLE_APP_CONFIG=/usr/local/bundle), else <root>/.bundle; - surgically strips socket-patch from the index, parsing only the exact dialect bundler's YAMLSerializer writes: plugin_paths/ load_paths keys, hooks subscriptions (dropping an event key left with no subscribers), commands/sources mappings — every other plugin's line is kept byte-verbatim, and unknown sections survive untouched; - deletes a bundler-plugin-installed copy only when the recorded dir sits inside the plugin root, never a path elsewhere; - deletes the index outright when no plugin remains (bundler treats a missing index as empty) and prunes the emptied plugin/app-config dirs (remove_dir, so a .bundle/config keeps its parent); - an index in any OTHER shape is never rewritten on a guess: the cleanup reports residue as a gem_plugin_registration files[] error whose message carries the remedy (`bundler plugin uninstall socket-patch`). The cleanup is reported as its own files[] entry (kind gem_plugin_registration, remove-only, emitted only when a registration existed) — contract + README updated; a project whose only leftover is the registration now counts as removable, so a re-run of setup --remove heals residue left by older CLIs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pes the delete gate Review finding (P2): Path::starts_with is a purely lexical component compare, so a crafted committed .bundle/plugin/index recording plugin_paths <root>/.bundle/plugin/../../victim passes the containment gate and remove_dir_all deletes outside the plugin root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fore deleting Path::starts_with is lexical, so the containment gate alone lets a crafted index's <plugin_root>/../../victim path through to remove_dir_all. Refuse any recorded dir containing a ParentDir component — bundler never writes traversal paths, so only hostile input is rejected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it-test path Review finding (P2): remove_plugin_directive read the ambient BUNDLE_APP_CONFIG, so the -core unit tests exercising it failed spuriously on machines exporting it (official ruby images export /usr/local/bundle) — and could have pointed the cleanup at a real machine-local index outside the tempdir. Thread the env through an explicit remove_plugin_directive_at (mirroring remove_plugin_registration_at); the public entry still reads the process env exactly like bundler. Verified red: with BUNDLE_APP_CONFIG=/usr/local/bundle, test_remove_clears_bundler_plugin_registration_entry failed before this change and passes after (69/69 with and without the export). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review nit: 5 rustfmt divergences introduced by this branch (the unquote condition width plus 4 test-code spots). cargo fmt --check on the workspace is back to main's pre-existing count; no touched-file divergence remains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 499e498. Configure here.
Bugbot round: the new tests' setup reads/writes carried bare unwrap(); failures now name the setup step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
enabled auto-merge (squash)
August 19, 2026 14:54
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Aug 19, 2026
Mikola Lysenko (mikolalysenko)
deleted the
fix/gem-setup-remove-plugin-registration
branch
August 19, 2026 15:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Defect
socket-patch setup --removeun-wired the Gemfile and deleted the generated plugin files, but left bundler's machine-local plugin registration (.bundle/plugin/index) dangling. Every laterbundle installthen prints bundler's block forever:with a misleading reinstall suggestion — on a project the user just cleanly ejected from.
Root cause
remove_plugin_directive(crates/socket-patch-core/src/setup/gem/update.rs:255) had only two steps: Gemfile un-wire +remove_plugin_files. Nothing ever touched the index bundler writes on first install (hook subscriptions inhooks:, plusplugin_paths:/load_paths:entries). Matches campaign REPORT.md root cause D3/C3 (dangling.bundle/pluginregistration aftersetup --remove).Campaign repro evidence
agent-b4/eject-setup-remove(bundler 4.0.18) re-run against a gem-b4 image rebuilt from this branch's binary under unique tags (socket-patch-test-{base,gem-b4}:f3-setup-remove; no:latesttouched):===EJECT-SETUP-REMOVE PASS===,out/i3.lognow contains zeropluginmentions (onlyBundle complete!), andstale .bundle/plugin dir present: no(wasyes+ the 5-line bundler block).f3-verify/(Dockerfile.gem-b4-f3 + b4 scenario copy with post-fixout/); images left in place for reviewer re-runs.YAMLSerializer), covered by unit tests + the real host-bundler 4.0.15 runtime test.Fix
remove_plugin_directivegains a third step,remove_plugin_registration(crates/socket-patch-core/src/setup/gem/mod.rs), which:Bundler.app_config_path($BUNDLE_APP_CONFIG, relative resolved against the project root, else<root>/.bundle);YAMLSerializerdialect (verified against the campaign's real 4.0.18 index) and strips socket-patch fromplugin_paths/load_paths/hooks/commands/sources, dropping hook events it empties, keeping every other plugin's lines byte-verbatim;gem_plugin_registrationfiles[]error naming the remedybundler plugin uninstall socket-patch(documented inCLI_CONTRACT.md; README--removerow updated).Scoped away from the sibling lane: no edits to
update.rs'sPLUGIN_BLOCK/add path or version-gate regions.Tests (red → green)
Red commit
e91f98f(tests only) fails onmain's production code exactly as designed; fix commitf8ee008turns it green:setup_matrix_gem::host_guard::gem_setup_roundtrip_host— seeds the campaign's verbatim index; pre-fix panic "remove must clear bundler's machine-local plugin registration".host_guard::gem_setup_remove_strips_registration_surgically_under_bundle_app_config— multi-plugin index underBUNDLE_APP_CONFIG: other plugin kept byte-verbatim, emptied hook event dropped.plugin_runtime::setup_remove_clears_bundler_plugin_registration— real host bundler 4.0.15:bundle installregisters →setup --remove→ next install must print no plugin warning.-coreunit tests (surgical strip, index deletion + dir prune, dry-run, unparseable-index residue-without-corruption, containment gate,Bundler.app_config_pathresolution incl. the ruby-image/usr/local/bundlecase) +update.rsenvelope-entry test.Review sign-off + follow-ups addressed
Independent reviewer verdict: approve, red/green independently verified via per-file baseline checkout (all 3 new tests fail on
da58dee's production code, 16/16 + 69/69 green at HEAD). The review's non-blocking findings are addressed in follow-up commits on this branch, red-first where a red exists:0a0295ered,11d007cfix):Path::starts_withis lexical, so a crafted committed index recording<plugin_root>/../../victimpassed the containment gate intoremove_dir_all. Red test proves the victim dir was deleted; fix rejects any recorded dir containing a..component.75084db):remove_plugin_directiveread ambientBUNDLE_APP_CONFIG, so-coreunit tests failed spuriously on machines exporting it (verified red withBUNDLE_APP_CONFIG=/usr/local/bundle) and could have touched a real machine-local index. Env now threaded through an explicitremove_plugin_directive_at; tests injectNone.499e498): the branch's 5 rustfmt divergences fixed; workspace--checkback to main's pre-existing count.Final gate (post-follow-ups)
cargo test -p socket-patch-core --lib setup::gem— 69 passed, 0 failed (also green withBUNDLE_APP_CONFIG=/usr/local/bundleexported)cargo test -p socket-patch-cli --features setup-e2e --test setup_matrix_gem— 16 passed (incl. the real-bundler leg)setup_invariants29 /cli_setup_silent6 /cli_parse_setup19 — all okcargo clippy --workspace --all-features -- -D warnings— clean🤖 Generated with Claude Code
Note
Medium Risk
Changes gem
setup --removefile I/O and index rewriting on a committed attacker-influenced path; traversal rejection and dialect refusal limit blast radius, and coverage includes real bundler e2e tests.Overview
Fixes incomplete gem eject:
setup --removeused to restore the Gemfile and delete.socket/bundler-pluginbut left bundler's machine-local plugin registration in.bundle/plugin/index, so every laterbundle installwarned about missing plugin paths.Third remove step: After Gemfile un-wire and plugin file deletion, removal now clears that registration via
remove_plugin_registration— resolving the index likeBundler.app_config_path($BUNDLE_APP_CONFIG, relative paths against the project root), parsing only bundler'sYAMLSerializerindex dialect, strippingsocket-patchfrom hooks/paths/commands while keeping other plugins byte-verbatim, atomically rewriting or deleting the index, and pruning empty dirs. Unparseable indexes are not guessed; cleanup failure surfaces agem_plugin_registrationfiles[]error with thebundler plugin uninstall socket-patchfallback.Safety & docs: Recorded install-dir deletion is containment-gated and rejects
..traversal in index paths. README andCLI_CONTRACT.mddocument the new behavior and JSONkind.Reviewed by Cursor Bugbot for commit 499e498. Configure here.