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.

Gem Version CI License: MIT

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_paths returns nil — not an empty array — when the plugin isn't installed. The if is what turns "plugin missing" into a quiet no-op instead of a crash. Do not replace it with a trailing rescue nil: that swallows real errors too, and you would get zero overrides with zero warning.
  • Keep the if defined? guard. On the very first bundle install, Bundler evaluates your whole Gemfile once before installing any plugin, just to find the plugin lines. During that pass force/ban don'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 doctor flags 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], npm overrides, pnpm/yarn resolutions — 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.