bundler-overrule
Force, ban, and swap gem versions in your Gemfile — the dependency overrides that Cargo (
[patch]), npm (overrides), and yarn (resolutions) have had for years, now for Bundler.
The problem
You try to upgrade one gem:
$ bundle update openssl
# ...nothing happens
Because somewhere in your Gemfile, web-push pins openssl ~> 2.2. Upgrading web-push
requires jwt ~> 3.0, which conflicts with four other gems pinned to jwt ~> 2.0 — some of
which haven't shipped a release in years. Your options today:
- fork every blocking gem just to relax a one-line version constraint, and maintain those forks forever, or
- point your Gemfile at git repos you don't control, or
- give up.
Pessimistic upper bounds (~>) are guesses about versions that didn't exist when they were
written — and they're very often wrong. Your Gemfile is your domain. You should get the
final say.
The fix
# Gemfile
source 'https://rubygems.org'
plugin 'bundler-overrule'
if (paths = Bundler::Plugin.index.load_paths('bundler-overrule'))
$LOAD_PATH.unshift(*paths)
require 'bundler-overrule'
end
gem 'rails'
gem 'web-push'
if defined?(Bundler::Overrule)
force 'openssl', '>= 3.0',
reason: 'web-push pins ~> 2.2 but works fine with 3.x (verified in CI)'
ban 'httpclient',
reason: 'unmaintained since 2016; vendored root certs expired'
end
$ bundle install
[bundler-overrule] 2 rules active — YOU own the consequences of these overrides:
force openssl >= 3.0 (was: web-push → ~> 2.2) # web-push pins ~> 2.2 but works fine with 3.x
ban httpclient (dropped from: jira-ruby) # unmaintained since 2016; vendored root certs expired
Done. No forks. No github: pins. One honest, self-documenting line per override.
Installation
$ bundle plugin install bundler-overrule
Then add the bootstrap block shown above to the top of your Gemfile.
Requires Ruby >= 3.1 and Bundler >= 2.6. Full tested matrix in CI.
Why the Gemfile needs those extra lines
Bundler plugins can't extend the Gemfile DSL on their own, so the require line is what makes
force and ban available while your Gemfile is being evaluated. Two details matter:
- Guard the
require.Bundler::Plugin.index.load_pathsreturnsnil— not an empty array — when the plugin isn't installed. Theifis what turns "plugin missing" into a quiet no-op instead of a crash. Do not replace it with a trailingrescue nil: that swallows real errors too, and you would get zero overrides with zero warning. - Keep the
if defined?guard. On the very firstbundle install, Bundler evaluates your whole Gemfile once before installing any plugin, just to find thepluginlines. During that passforce/bandon't exist yet. The guard skips them; the real evaluation pass a moment later has the plugin loaded and applies them.
Usage
force NAME, REQUIREMENT, reason: nil
Overrides every constraint on NAME — from other gems' gemspecs and from your own
Gemfile — with REQUIREMENT. Bundler resolves as if everyone had asked for your version.
force 'nokogiri', '~> 1.18'
force 'rack', '>= 3.0', reason: 'CVE-2025-XXXXX; sinatra pin is stale'
ban NAME, reason: nil
Removes NAME from dependency resolution entirely. Useful for transitively-pulled gems you
know your code paths never hit, or abandoned gems you've replaced.
ban 'mimemagic', reason: 'we use marcel'
Banning a gem your Gemfile requires directly is an error — remove the gem line instead.
Block form
Sugar for grouping; same registry underneath.
overrule do
force 'openssl', '>= 3.0'
ban 'httpclient'
end
swap OLD, with: NEW (v0.3 — roadmap)
Substitute a maintained, API-compatible fork published under a different name:
swap 'httpclient', with: 'byroot-httpclient'
Inspecting active rules
$ bundle overrule list # every rule + exactly which constraints it rewrote
$ bundle overrule doctor # stale rules, unsupported Bundler, missing bootstrap line
doctor exits non-zero when it finds a problem, so it works as a CI check.
How it works
bundler-overrule is a standard Bundler plugin.
When (and only when) you declare a rule, it filters the dependency lists that gem specifications
report to Bundler's resolver: force rewrites matching dependency requirements, ban drops
them. The resolver itself is untouched, and your Gemfile.lock is a perfectly ordinary
lockfile — deployment mode, --frozen, and CI caching all work unchanged.
With no rules declared, the plugin applies zero patches and your resolution is byte-identical to vanilla Bundler (we test this — see scenario B7).
Each run writes .bundle/overrule-report.json recording which edges were rewritten. That file
is what list and doctor read; it is safe to delete and safe to gitignore. Nothing
overrule-specific is ever written into Gemfile.lock.
Use responsibly
An override is you telling Bundler "I know better than this gem's author." Sometimes you do — upper bounds are written before the future exists. But:
- Test the result. You are opting out of the maintainer's compatibility promise.
- Write the
reason:. Your future self and your teammates will thank you. - Don't report bugs upstream for gem combinations their maintainers never declared support for — reproduce without the override first.
- Prefer deleting a rule once upstream relaxes the constraint (
bundle overrule doctorflags rules that no longer have any effect).
Compatibility
| | | |---|---| | Ruby | 3.1, 3.2, 3.3, 3.4 | | Bundler | 2.6, 2.7, 4.0 (weekly CI run against latest) | | OS | Linux, macOS (Windows: best effort, CI planned) |
Bundler 2.4 and 2.5 are not supported: they lack the internals this plugin relies on. There is no Bundler 3.x — the line went 2.7 → 4.0.
If a new Bundler release moves the internals we patch, the plugin fails loudly with a clear message rather than silently mis-resolving. Pin your Bundler version in CI like you already should.
Roadmap
- [x] v0.1 —
force, warnings,overrule list - [x] v0.2 —
ban,overrule doctor - [ ] v0.3 —
swap(gem substitution) - [ ] Windows CI
Prior art & credits
- byroot's "The Missing Bundler Features" — this gem implements the
force/ban/substitute semantics proposed there. - Longstanding Bundler feature requests: rubygems/bundler#1549 (2011), #2412 (2013), rubygems/rfcs#54.
- bundler-override and ManageIQ's bundler-inject — earlier takes on the same pain.
- Cargo
[patch], npmoverrides, pnpm/yarnresolutions— proof this belongs in every package manager.
Contributing
Bug reports and PRs welcome — see CONTRIBUTING.md. Quick start:
$ git clone https://github.com/TheSoloHacker47/bundler-overrule && cd bundler-overrule
$ bin/setup
$ bundle exec rake spec integration
License
MIT.