skip to content

In Ruby under Bundler, why does shelling out from one bundled process to another project's `bundle exec` load the wrong Gemfile, and how does `Bundler.with_unbundled_env` fix it?

level: seniorimportance: should knowfreq 24%

answer

  1. the child inherits the parent's bundle
  2. BUNDLE_GEMFILE and RUBYOPT leak down
  3. with_unbundled_env strips BUNDLE_ keys
  4. with_original_env keeps your own settings
  5. with_clean_env was removed

basics

~10 s

A process running under Bundler exports BUNDLE_GEMFILE and -rbundler/setup in RUBYOPT, and every child inherits them, so the other project's bundle exec reuses the parent's Gemfile. Bundler.with_unbundled_env runs a block with those variables removed.

solid answer

~40 s

`bundle exec` and `Bundler.setup` leave marks in `ENV`: `BUNDLE_GEMFILE` names the parent's Gemfile, `RUBYOPT` carries `-rbundler/setup`, `RUBYLIB` and `PATH` point at Bundler and the bundle's executables. `system`, backticks and `Process.spawn` pass `ENV` to the child, so a Ruby script in another project starts inside the *parent's* bundle, and its own `bundle exec` finds the inherited `BUNDLE_GEMFILE` instead of searching for its Gemfile. `Bundler.with_unbundled_env { ... }` runs the block with the environment as it was before Bundler was activated, minus every `BUNDLE_*` key and Bundler's `RUBYOPT` and `RUBYLIB` entries, then restores `ENV`. `Bundler.with_original_env` restores the pre-Bundler environment but keeps any `BUNDLE_*` values you had set yourself. `Bundler.unbundled_system` and `unbundled_exec` wrap `system` and `exec`. The Bundler 1 names `with_clean_env` and `clean_env` now raise and point to these.

code

ruby · 8 lines
ruby
# Rakefile task in project A, run with bundle exec
task :build_billing do
  Bundler.with_unbundled_env do
    Dir.chdir("../billing") do
      system("bundle", "exec", "rake", "build", exception: true)
    end
  end
end

go deeper

for a junior

Recall that processes started from a bundled program inherit its Bundler environment, and that Bundler.with_unbundled_env runs a block without it.

for a middle

Explain which variables carry the bundle into children, BUNDLE_GEMFILE, RUBYOPT, RUBYLIB and PATH, and why an inherited BUNDLE_GEMFILE beats the Gemfile search.

for a senior

Diagnose cross-project tooling that loads the wrong gems, choose between unbundled and original environments deliberately, and use BUNDLE_GEMFILE on purpose for multi-Gemfile test matrices.

for a principal

Design build and release tooling that spans many repositories so each step runs in its own bundle, isolating environments rather than relying on each script to clean up.

## How the bundle leaks into child processes When a command starts through `bundle exec`, Bundler records the bundle in **environment variables** so every Ruby process the command spawns stays inside it: | Variable | Value under Bundler | |---|---| | `BUNDLE_GEMFILE` | absolute path of the parent's Gemfile | | `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP` | details of the same bundle | | `RUBYOPT` | includes `-r.../bundler/setup` | | `RUBYLIB` | Bundler's own `lib` directory first | | `PATH` | the bundle's executable directory first | That is exactly right for `bundle exec rake` spawning `ruby worker.rb` in the same project. It is exactly wrong when a tool running in project A shells out to project B: ```ruby # a release script in project A, started with bundle exec Dir.chdir("../billing") { system("bundle exec rake build") } ``` The child inherits `BUNDLE_GEMFILE=/path/to/A/Gemfile`. Bundler normally searches upward from the working directory for a Gemfile, but an explicit `BUNDLE_GEMFILE` wins, so project B's `rake` runs with project A's gems. The symptoms: missing gems, the wrong version of a shared gem, or a baffling "already activated" error. ## The fix: `Bundler.with_unbundled_env` ```ruby Bundler.with_unbundled_env do Dir.chdir("../billing") { system("bundle exec rake build") } end ``` Inside the block, `ENV` is replaced with a cleaned copy of the environment from **before Bundler was activated**: 1. Start from the original environment. Bundler saved the pre-`bundle exec` values of `PATH`, `RUBYOPT`, `RUBYLIB`, `GEM_HOME`, `GEM_PATH` and the `BUNDLE_*` keys it changed. 2. Delete **every** key starting with `BUNDLE_`, plus `BUNDLER_SETUP`. 3. Remove `-rbundler/setup` from `RUBYOPT` and Bundler's directory from `RUBYLIB`. After the block, `ENV` is restored. The child now behaves as if typed in a fresh shell: its own `bundle exec` finds project B's Gemfile. ## The related helpers - **`Bundler.with_original_env`** - the pre-Bundler environment *without* step 2, so `BUNDLE_*` values you exported yourself (say `BUNDLE_PATH` for CI) survive. Use it when the child should inherit your own settings but not the parent's bundle. - **`Bundler.unbundled_system(cmd)`** and **`Bundler.unbundled_exec(cmd)`** - shorthands for calling `system` or `exec` inside `with_unbundled_env`; `original_system` and `original_exec` do the same with `with_original_env`. - **`Bundler.unbundled_env`** returns the cleaned hash without switching `ENV`, useful for passing an explicit environment to a spawn call. The Bundler 1 names are gone: `Bundler.with_clean_env` and `Bundler.clean_env` raise, saying they were removed in favour of `with_unbundled_env` and `unbundled_env`. ## Diagnosing a leaked bundle When a child process loads unexpected gems, check what it inherited before changing any code: 1. Print the Bundler-related variables in the child, for example `ENV.select { |k, _| k.start_with?("BUNDLE") }` together with `ENV["RUBYOPT"]`. 2. Compare `BUNDLE_GEMFILE` with the directory the child runs in; a mismatch is the leak. 3. In the parent, `Bundler.original_env` shows what the environment looked like before Bundler touched it, which tells you whether a `BUNDLE_*` value came from Bundler or from your own shell. The same inheritance explains a subtler symptom: a child Ruby process that is not a Bundler project at all, such as a system tool written in Ruby, fails because `-rbundler/setup` in `RUBYOPT` forces it into the parent's bundle, where its own gems are missing. The bundle-exec man page gives this case, with a Homebrew command as its example, alongside shelling out to a different bundle. ## Choosing a Gemfile on purpose `BUNDLE_GEMFILE` is also the supported way to run one project against several dependency sets: ```bash BUNDLE_GEMFILE=gemfiles/rack_3_1.gemfile bundle install BUNDLE_GEMFILE=gemfiles/rack_3_1.gemfile bundle exec rake test ``` The lockfile follows the Gemfile name (`gemfiles/rack_3_1.gemfile.lock`), and the Gemfile's directory becomes the project root for relative paths. `bundle exec --gemfile=...` and `bundle install --gemfile=...` do the same for a single command. ## Traps - Calling `bundle exec` from a Git hook or editor task that itself runs under another bundle. - Using `with_original_env` and expecting a clean slate; a `BUNDLE_GEMFILE` you exported earlier survives it. - Forgetting `Dir.chdir` or `chdir:`: even with a clean environment, Bundler searches for the Gemfile from the child's working directory.

  • When should you choose `Bundler.with_original_env` over `with_unbundled_env`?
    When the child should keep `BUNDLE_*` settings that existed before Bundler started, for example a `BUNDLE_PATH` or credentials exported by CI. `with_original_env` restores the pre-Bundler environment as is; `with_unbundled_env` additionally deletes every `BUNDLE_` key. If one of those was a `BUNDLE_GEMFILE` you exported yourself, only the unbundled form drops it.
  • A gem's test suite runs against three Rack versions. How does `BUNDLE_GEMFILE` help?
    Keep one Gemfile per dependency set, such as `gemfiles/rack_3_1.gemfile`, and run `BUNDLE_GEMFILE=gemfiles/rack_3_1.gemfile bundle exec rake test` for each. Bundler uses that Gemfile and a lockfile named after it, and the Gemfile's directory becomes the root for relative paths, so each combination resolves and locks separately.

saying these in an interview costs you the question

  • A child process always starts outside the parent's bundle
  • bundle exec in another directory always finds that directory's Gemfile
  • with_clean_env is the current name for with_unbundled_env
  • with_unbundled_env permanently strips Bundler variables from ENV
  • with_original_env and with_unbundled_env produce identical environments