I'm Hiroshi Shibata (hsbt), a Ruby committer and the lead maintainer of RubyGems and Bundler.
TL;DR
RubyGems and Bundler 4.1.0.beta2 shipped on October 8, 2026. This is Part 1 of a three-part series on what's new in 4.1, and it covers two features. override in the Gemfile rewrites version constraints declared by your dependencies, transitive ones included, and can even drop or replace a dependency. The prune setting makes Bundler delete download caches and .git directories after install, replacing the rm -rf line in your Dockerfile. Both hand back to you a decision RubyGems and Bundler used to make on their own, and both are opt-in. Nothing changes until you ask for it.
override in the Gemfile
When an upper bound gets in your way
A gem you depend on declares something like < 2.0. You know the newer version works, but the resolver still refuses. The same thing happens with Ruby itself. If a gemspec's required_ruby_version has an upper bound like < 4.1, the gem stops installing the moment you upgrade to Ruby 4.1, even if it actually works.
Preview releases hide this problem. RubyGems sees a preview or rc Ruby as a prerelease version like 4.1.0.preview1, which satisfies < 4.1. You can test everything on the preview and still hit the wall on release day.
Until now, you could fork the gem and maintain it yourself, or wait for upstream to relax the constraint. Both cost far too much for a single line.
The request is old. ruby/rubygems#4178 was opened in December 2020 and stayed open until May 2026, a week after override was merged. I also tried adding a setting like ignore_ruby_upper_bounds in ruby/rubygems#9454, but byroot and Edouard-chin pointed out in review that this belonged in the Gemfile DSL. After the discussion in ruby/rubygems#9494, we settled on the current override.
Syntax
override <target>, <field>: <operation>
The target is a gem name string or :all. The field is version:, required_ruby_version:, or required_rubygems_version:, for the dependency's version requirement, the Ruby version requirement, and the RubyGems version requirement. There are three operations.
- A requirement string replaces the existing constraint entirely. The original constraint is discarded, whether it comes from a direct or a transitive dependency.
-
:ignore_upperremoves the upper-bound operators<and<=.~>collapses into a>=that keeps only the lower bound. -
nildrops every constraint and leaves>= 0.
Here is what each operation does.
Original Operation Result
>= 1.0, < 2.0 ">= 8.0" >= 8.0
~> 1.5 :ignore_upper >= 1.5
>= 1.0, < 2.0 :ignore_upper >= 1.0
!= 1.2, < 2.0 :ignore_upper != 1.2
= 0.9.1 nil >= 0
Keeping != 1.2 under :ignore_upper is intentional. The first implementation used an allow-list that kept only lower bounds, but that brought back versions the user had explicitly excluded, such as one with a known vulnerability. Review caught it, and I switched to a reject-list that drops only the upper-bound operators.
It reaches transitive dependencies
What makes override worth having is that it works on dependencies you never wrote yourself. Say qux depends on bar (< 2.0), and bar 2.0 is out but you can't use it.
source "https://rubygems.org"
gem "qux"
Resolving this Gemfile stops bar at 1.0.
bar (1.0)
qux (1.0)
bar (< 2.0)
Add one line of override.
source "https://rubygems.org"
override "bar", version: :ignore_upper
gem "qux"
Run bundle update bar, and bar moves to 2.0.
bar (2.0)
qux (1.0)
bar (< 2.0)
The gemspec of qux hasn't changed, so the lockfile still records bar (< 2.0) under it. Yet the resolved version is 2.0.
The lockfile doesn't record override
That lockfile looks contradictory because override itself is never written to it. Gemfile.lock holds only the result of resolution, not which constraints were rewritten or how. This was a design decision from byroot's review. The Gemfile already holds the override, so recording it again in the lockfile adds no information.
Since nothing is recorded, adding an override to an existing lockfile keeps the locked version as long as it satisfies the rewritten constraint. That's why the example above needed bundle update bar. Bundler can't tell from the lockfile whether the override was just added, and re-resolving whenever one is present would update gems on every bundle install. If the locked version no longer satisfies the constraint, as with version: ">= 2.0", Bundler re-resolves right away.
The flip side is that the lockfile alone can't tell you why a particular version was chosen. When you use override, leave the reason as a comment in the Gemfile. Whoever touches it next will thank you.
When resolution fails, Bundler lists the active overrides at the end of the error.
Bundler applied the following overrides while resolving:
override "bar", version: "= 3.0" (declared at Gemfile:2)
Ruby and RubyGems version requirements
The other two fields apply to the Ruby and RubyGems requirements a gem declares in its metadata. They're for the moment right after a Ruby upgrade, when a gem that hasn't caught up blocks you with its required_ruby_version.
override :all, required_ruby_version: :ignore_upper
Only these two fields accept :all, which targets every gem at once. :all with version: is rejected, because a dependency's version requirement only makes sense per gem. When a gem has both a per-gem entry and an :all entry, the per-gem one wins.
As with version:, adding this to an existing lockfile keeps the locked versions.
1. Before adding selectable (1.0)
2. Add :all, run bundle lock selectable (1.0)
3. Then run bundle update selectable (2.0)
4. Resolve without a lockfile selectable (2.0)
:all covers every gem in one line, so without this behavior it would move unrelated gems too.
Dropping or replacing a dependency with from: and to:
4.1.0.beta2 adds from: and to:, which rewrite the dependency itself instead of its version constraint. This started with byroot asking in ruby/rubygems#9517 whether there was a way to avoid installing a problematic dependency at all. For example, jekyll only loads em-websocket for jekyll serve --livereload, yet always installs it.
Let's go back to qux and bar. To drop the dependency of qux on bar, set from: to the depending gem and to: to nil.
source "https://rubygems.org"
override "bar", from: "qux", to: nil
gem "qux"
After resolving, bar disappears from the lockfile's gem list.
qux (1.0)
bar (< 2.0)
bar (< 2.0) stays under qux as published, because the lockfile records a gem's declaration without rewriting it, just as with version overrides. Remove the override line and resolve again, and bar comes back at 1.0.
To replace it with another gem, put that gem's name in to:. This is for cases where a lighter, API-compatible gem exists.
source "https://rubygems.org"
override "bar", from: "qux", to: "bar-lite"
gem "qux"
bar-lite (1.0)
qux (1.0)
bar (< 2.0)
The < 2.0 that qux put on bar is dropped. If the replacement needs a version constraint, add version: alongside, as in to: "bar-lite", version: ">= 2.0".
Bundler doesn't look at what a gem requires at runtime. If qux loads bar unconditionally, dropping it gives you a LoadError. A replacement only works when it provides the files qux loads. The replacement also doesn't take the original name, so bar won't appear in Gem.loaded_specs.
An override that would leave the original gem in the bundle anyway is an error. You can't drop a gem listed directly in the Gemfile. If you try to replace bar while another gem still depends on it, Bundler asks you to write an override for each dependent.
override "bar", from: "qux", to: "bar-lite" (declared at Gemfile:2) would leave
both bar and bar-lite in the bundle because bar is still required by:
corge
Replace it for each of them, for example:
override "bar", from: "corge", to: "bar-lite"
prune in Bundler
Turning the Dockerfile rm -rf into a setting
The Dockerfile that Rails generates contains this line.
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
After bundle install, it deletes the download cache and the .git directories of git gems. Neither needs to stay in the image, and keeping them only wastes space. prune moves this hand-written cleanup into Bundler. It was requested in ruby/rubygems#7018 back in 2020 and landed in ruby/rubygems#9815.
prune only removes artifacts that Bundler creates for its own purposes and can rebuild from the lockfile. It never touches gem contents. There are two categories. cache is the cache directory under the bundle path, which holds both .gem files and git mirrors. git is the .git directory of checked-out git gems. These match the two parts of the rm -rf above that Bundler can handle.
Usage
bundle config set --local prune cache:git
The environment variable is BUNDLE_PRUNE. Categories can be separated by spaces or :.
BUNDLE_PRUNE=cache:git bundle install
Any value that isn't a category name is read as a boolean, and a true value enables every category. byroot asked for this. A Dockerfile or buildpack can set BUNDLE_PRUNE=1 without knowing the category names and pick up new categories as they're added.
One thing to watch out for. Only false, f, no, n, 0, and the empty string count as false. This vocabulary is shared by every Bundler setting, so BUNDLE_PRUNE=off and BUNDLE_PRUNE=none are true, which means prune everything.
Pruning runs after a successful bundle install, bundle update, or bundle cache, and during bundle clean.
It needs a bundle path
If you set prune without configuring a bundle path, nothing is deleted and you get only a warning.
The `prune` setting was ignored because this bundle installs into the system gem
directory, which Bundler shares with RubyGems. Run `bundle config set --local
path vendor/bundle` to prune.
In Bundler 4, path is unset by default, and gems go into the system gem directory. Its cache is shared with RubyGems and also holds gem files that gem install put there without Bundler knowing. If prune deleted those, the damage would reach beyond what Bundler manages, so it only runs when a bundle path is set. Once Bundler 5 makes .bundle the default path, this guard will simply stop firing.
Getting things back after pruning
prune only deletes things that can be regenerated, but they don't come back on their own. Pruned artifacts return only when Bundler reinstalls the gem. A plain bundle install fetches nothing for gems that are already installed, so the cache stays empty. Use bundle install --redownload to fetch them again. bundle pristine works once the cache is back. If the prune setting is still there, though, they get deleted again at the end of the reinstall.
no_prune is now keep_outdated_cache
There's an existing setting with a similar name, no_prune. It controls whether bundle cache removes outdated gems from vendor/cache, and it has nothing to do with the new prune. The name reads as if it disabled the new prune, so ruby/rubygems#9816 renamed it to keep_outdated_cache. The polarity is the same, and true still means keep. The old name is still read, but it prints a deprecation warning.
[DEPRECATED] The `no_prune` setting has been renamed to `keep_outdated_cache` and will be removed in Bundler 5. Use `keep_outdated_cache` instead.
The old name goes away in Bundler 5, so if your config has BUNDLE_NO_PRUNE, update it now.
Try It
Neither override nor prune changes anything just because you upgrade to 4.1. Until now, RubyGems and Bundler decided on their own which constraints to resolve against and which install artifacts to keep. With 4.1, people who need to can make those calls themselves.
4.1.0.beta2 is on rubygems.org, so you can try it today.
gem update --system 4.1.0.beta2
gem install bundler -v 4.1.0.beta2
If anything behaves oddly during the beta, please report it to ruby/rubygems. Parts 2 and 3 will cover the rest of the 4.1 features.
Notes
- Written in October 2026 against 4.1.0.beta2. Details may change before the final release.
Top comments (0)