Skip to content

bin/dev: scrub foreign bundler activation before Ruby boots - #95

Merged
JPDuchesne merged 4 commits into
mainfrom
jpd/94-self-defending-entrypoint
Aug 3, 2026
Merged

bin/dev: scrub foreign bundler activation before Ruby boots#95
JPDuchesne merged 4 commits into
mainfrom
jpd/94-self-defending-entrypoint

Conversation

@JPDuchesne

Copy link
Copy Markdown
Contributor

Summary

  • The bin/dev sh shim now unsets the bundler-activation keys (RUBYOPT, RUBYLIB, BUNDLE_GEMFILE, BUNDLE_PATH, BUNDLE_APP_CONFIG, BUNDLE_BIN, BUNDLE_BIN_PATH, BUNDLER_VERSION, BUNDLER_SETUP) before probing for or exec'ing Ruby — a leaked RUBYOPT=-r.../bundler/setup is processed by the interpreter ahead of the script's first line, so the sh layer is the only place this defense can live.
  • GEM_HOME/GEM_PATH deliberately stay: they're legitimate user config, and the one resolution they can redirect into an ephemeral cache is guarded at its call site (GemSkillLinker: links minted under a sandboxed session point into ephemeral sandbox cache paths #90).
  • New integration tests spawn the real shim: dev boots to its normal no-dev.yml refusal under a hostile bundle-exec env (instead of crashing in the caller's bundler), and a dev.yml command's child sees neither RUBYOPT nor BUNDLE_GEMFILE.

Why

Third member of the harness-env-leak family (ai-flow#38, ai-flow#44, dev#89). Every caller patching every spawn site is a discipline that keeps failing — ai-flow#44 happened precisely because a later-added shell-out missed the existing scrub. dev defending itself at its entrypoint retires the class for every caller: ai-flow, sandboxed sessions, CI, anything future.

Test plan

  • New Dev::BinDevTest red without the shim change, green with it
  • Full suite (792 tests), rubocop, srb tc all green

Closes #94

Made with Cursor

A harness running under bundle exec leaks RUBYOPT/BUNDLE_* into every
child, and the interpreter activates the caller's bundle before dev's
Ruby half runs a single line — dev then can't load its own gems
(observed live as ai-flow#44's LoadError). dev picks its own Ruby and
gems; no caller's activation is ever wanted, so the sh shim unsets the
activation keys itself, making every caller's scrub defense-in-depth
instead of load-bearing.

Closes #94

Co-authored-by: Cursor <cursoragent@cursor.com>
The dev.yml probe dragged shadowenv provisioning into the test, which
fails on CI runners; a PATH-stubbed ruby printing its env pins the
exact unset list hermetically.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

JPDuchesne and others added 2 commits August 3, 2026 19:28
The suite's own bundle exec activation is the live truth: any key where
ENV differs from Bundler.original_env is something the locked bundler
exported, so a bundler bump that exports a new activation key turns the
build red naming it. Subset direction only — config-dependent keys the
launch didn't export are free no-ops, so exact equality would just add
flake. The guard caught its first drift before shipping: bundler 4
exports BUNDLE_LOCKFILE, which the hand-pinned list missed.

Co-authored-by: Cursor <cursoragent@cursor.com>
The sh shim is the single source of truth for the scrub list (the unset
must run before any Ruby exists), so the tests alias it by parsing
bin/dev instead of keeping a pinned copy that could drift; the stub-ruby
and env-parsing mechanics move into named helpers and the file comment
states what each of the three tests proves.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne
JPDuchesne merged commit d17b2ff into main Aug 3, 2026
5 checks passed
@JPDuchesne
JPDuchesne deleted the jpd/94-self-defending-entrypoint branch August 3, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bin/dev: self-defending entrypoint — scrub foreign bundler activation before Ruby boots

1 participant