GeezerVison is showing github.com — fix(gem): setup --remove clears bundler's machine-local .bundle/plugin registration by mikolalysenko · Pull Request #210 · SocketDev/socket-patch
Conversation
Defect
socket-patch setup --remove un-wired the Gemfile and deleted the generated plugin files, but left bundler's machine-local plugin registration (.bundle/plugin/index) dangling. Every later bundle install then prints bundler's block forever:
The following plugin paths don't exist: .../.socket/bundler-plugin
... Continuing without installing plugin socket-patch
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 in hooks:, plus plugin_paths:/load_paths: entries). Matches campaign REPORT.md root cause D3/C3 (dangling .bundle/plugin registration after setup --remove).
Campaign repro evidence
- Original repro
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 :latest touched): ===EJECT-SETUP-REMOVE PASS===, out/i3.log now contains zero plugin mentions (only Bundle complete!), and stale .bundle/plugin dir present: no (was yes + the 5-line bundler block).
- Verification artifacts: session scratchpad
f3-verify/ (Dockerfile.gem-b4-f3 + b4 scenario copy with post-fix out/); images left in place for reviewer re-runs.
- agent-b2 (bundler 2.7.2) shows the same residue pre-fix; the 2.7/4.0 index dialect is identical (bundler
YAMLSerializer), covered by unit tests + the real host-bundler 4.0.15 runtime test.
Fix
remove_plugin_directive gains a third step, remove_plugin_registration (crates/socket-patch-core/src/setup/gem/mod.rs), which:
- resolves the index exactly like
Bundler.app_config_path ($BUNDLE_APP_CONFIG, relative resolved against the project root, else <root>/.bundle);
- parses only bundler's
YAMLSerializer dialect (verified against the campaign's real 4.0.18 index) and strips socket-patch from plugin_paths/load_paths/hooks/commands/sources, dropping hook events it empties, keeping every other plugin's lines byte-verbatim;
- deletes a bundler-installed plugin dir only when inside the plugin root; deletes the index when no plugin remains (bundler regenerates on demand) and prunes emptied dirs; rewrites atomically when other plugins remain; dry-run writes nothing;
- never rewrites an unrecognized index shape — that surfaces as a new remove-only
gem_plugin_registration files[] error naming the remedy bundler plugin uninstall socket-patch (documented in CLI_CONTRACT.md; README --remove row updated).
Scoped away from the sibling lane: no edits to update.rs's PLUGIN_BLOCK/add path or version-gate regions.
Tests (red → green)
Red commit e91f98f (tests only) fails on main's production code exactly as designed; fix commit f8ee008 turns 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 under BUNDLE_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 install registers → setup --remove → next install must print no plugin warning.
- 14 new
-core unit tests (surgical strip, index deletion + dir prune, dry-run, unparseable-index residue-without-corruption, containment gate, Bundler.app_config_path resolution incl. the ruby-image /usr/local/bundle case) + update.rs envelope-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:
- P2 traversal (
0a0295e red, 11d007c fix): Path::starts_with is lexical, so a crafted committed index recording <plugin_root>/../../victim passed the containment gate into remove_dir_all. Red test proves the victim dir was deleted; fix rejects any recorded dir containing a .. component.
- P2 hermeticity (
75084db): remove_plugin_directive read ambient BUNDLE_APP_CONFIG, so -core unit tests failed spuriously on machines exporting it (verified red with BUNDLE_APP_CONFIG=/usr/local/bundle) and could have touched a real machine-local index. Env now threaded through an explicit remove_plugin_directive_at; tests inject None.
- nit fmt (
499e498): the branch's 5 rustfmt divergences fixed; workspace --check back 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 with BUNDLE_APP_CONFIG=/usr/local/bundle exported)
cargo test -p socket-patch-cli --features setup-e2e --test setup_matrix_gem — 16 passed (incl. the real-bundler leg)
setup_invariants 29 / cli_setup_silent 6 / cli_parse_setup 19 — all ok
cargo clippy --workspace --all-features -- -D warnings — clean
🤖 Generated with Claude Code
Note Medium Risk
Changes gem setup --remove file 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 --remove used to restore the Gemfile and delete .socket/bundler-plugin but left bundler's machine-local plugin registration in .bundle/plugin/index, so every later bundle install warned 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 like Bundler.app_config_path ($BUNDLE_APP_CONFIG, relative paths against the project root), parsing only bundler's YAMLSerializer index dialect, stripping socket-patch from 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 a gem_plugin_registration files[] error with the bundler plugin uninstall socket-patch fallback.
Safety & docs: Recorded install-dir deletion is containment-gated and rejects .. traversal in index paths. README and CLI_CONTRACT.md document the new behavior and JSON kind.
Reviewed by Cursor Bugbot for commit 499e498. Configure here.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
There was a problem hiding this comment.
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 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.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Bugbot round: the new tests' setup reads/writes carried bare unwrap();
failures now name the setup step.
Co-Authored-By: Claude Fable 5 <[email protected]>
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Oct 9, 2026
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|