Skip to content

[Bug]brew bundle --cleanup triggers autoremove which ignores Brewfile #21350

@AbdelrahmanHafez

Description

@AbdelrahmanHafez

brew doctor output

Warning: Some installed casks are deprecated or disabled.
You should find replacements for the following casks:
  chromedriver
  inkscape
  prowlarr
  radarr
  sonarr

Verification

  • I ran brew update twice and am still able to reproduce my issue.
  • My "brew doctor output" above says Your system is ready to brew or a definitely unrelated Tier message.
  • This issue's title and/or description do not reference a single formula e.g. brew install wget. If they do, open an issue at https://github.com/Homebrew/homebrew-core/issues/new/choose instead.

brew config output

HOMEBREW_VERSION: 5.0.8-15-g7543440
ORIGIN: https://github.com/Homebrew/brew
HEAD: 754344022ff6b44f759c04b828fed0e9a76e3216
Last commit: 35 hours ago
Branch: main
Core tap JSON: 02 Jan 00:33 UTC
Core cask tap JSON: 02 Jan 00:33 UTC
HOMEBREW_PREFIX: /opt/homebrew
HOMEBREW_CASK_OPTS: []
HOMEBREW_DOWNLOAD_CONCURRENCY: 20
HOMEBREW_FORBID_PACKAGES_FROM_PATHS: set
HOMEBREW_MAKE_JOBS: 10
HOMEBREW_SORBET_RUNTIME: set
Homebrew Ruby: 3.4.8 => /opt/homebrew/Library/Homebrew/vendor/portable-ruby/3.4.8/bin/ruby
CPU: deca-core 64-bit arm_firestorm_icestorm
Clang: 17.0.0 build 1700
Git: 2.50.1 => /Library/Developer/CommandLineTools/usr/bin/git
Curl: 8.7.1 => /usr/bin/curl
macOS: 26.2-arm64
CLT: 26.2.0.0.1.1764812424
Xcode: N/A
Rosetta 2: false

What were you trying to do (and why)?

I'm using brew bundle --cleanup to keep my system in sync with a Brewfile (declarative package management). The --cleanup flag should remove packages not in the Brewfile while preserving packages that are in the Brewfile.

What happened (include all command output)?

Packages explicitly listed in my Brewfile were removed by brew bundle --cleanup.

Specifically, tree was removed even though it was in my Brewfile:

brew "tree"

After investigation, I found this happens when:

  1. A package is explicitly in the Brewfile
  2. That package has installed_on_request: false (was originally installed as a dependency)
  3. The package it was a dependency of is not in the Brewfile

The --cleanup operation removes the "parent" package, which triggers autoremove. The autoremove process doesn't know about the Brewfile, so it removes packages that appear to be orphaned dependencies, even if they're explicitly listed in the Brewfile.

What did you expect to happen?

Packages explicitly listed in the Brewfile should not be removed by brew bundle --cleanup, regardless of how they were originally installed (on request vs as dependency).

Step-by-step reproduction instructions (by running brew commands)

# 1. Install a package that has dependencies
brew install yt-dlp

# 2. Verify deno was installed as a dependency
brew info --json=v2 deno | jq '.formulae[0].installed[0].installed_on_request'
# Returns: false

# 3. Verify deno is only used by yt-dlp
brew uses --installed deno
# Returns: yt-dlp

# 4. Create a test Brewfile that has deno but not yt-dlp
echo 'brew "deno"' > /tmp/test-brewfile

# 5. Run cleanup dry-run to see what would be removed
brew bundle cleanup --file=/tmp/test-brewfile
# Output shows "Would uninstall formulae: yt-dlp" (correct - not in Brewfile)
# Note: deno does NOT appear here because the dry-run only shows direct removals,
# not the cascade effect of autoremove that happens afterward.

# 6. TRIGGER THE BUG - run with --force
# WARNING: This will uninstall everything not in the test Brewfile!
# Only run this if you're prepared to reinstall your packages afterward.
brew bundle cleanup --file=/tmp/test-brewfile --force
# Output: "Uninstalled 1 formula" (yt-dlp)
# But then autoremove kicks in and silently removes deno too!

# 7. Verify deno was incorrectly removed
brew list deno
# Error: No such keg: /opt/homebrew/Cellar/deno
# BUG: deno was in the Brewfile but got removed anyway!

Why the dry-run is misleading: The dry-run only shows what brew bundle cleanup will directly uninstall. It cannot predict what autoremove will do afterward. The bug is a cascade effect: removing yt-dlp makes deno appear "orphaned" to autoremove, which then removes it despite being in the Brewfile.

Note: The bug also affects brew bundle --cleanup and brew bundle install --cleanup because these flags run cleanup with force: true hardcoded (no dry-run).

Root Cause Analysis

The issue is in the interaction between brew bundle cleanup and Homebrew's autoremove:

  1. brew bundle --cleanup calls brew uninstall for packages not in Brewfile
  2. Uninstalling triggers Homebrew's install cleanup (autoremove)
  3. autoremove looks at installed_on_request flag to find orphans
  4. autoremove has no knowledge of the Brewfile
  5. Packages with installed_on_request: false are removed even if in Brewfile

Workaround

HOMEBREW_NO_INSTALL_CLEANUP=1 brew bundle --cleanup --file=~/.Brewfile

Suggested Fix

Option 1: brew bundle sets HOMEBREW_NO_AUTOREMOVE=1 during cleanup operations

  • Pros: Simple, self-contained change in bundle code
  • Cons: Disables autoremove entirely, so genuinely orphaned dependencies (not in Brewfile) won't be cleaned up either

Option 2: brew bundle marks Brewfile packages as installed_on_request: true before running cleanup

  • Pros: Uses existing Homebrew mechanism
  • Cons: Persists metadata changes to disk, which is a side effect cleanup shouldn't have

Option 3: autoremove accepts a "protected packages" list that brew bundle can pass

  • Pros: Surgical fix that lets autoremove still clean up truly orphaned packages while respecting the Brewfile
  • Cons: Requires changes to core autoremove logic, not just bundle

Option 3 is the cleanest solution as it addresses the root cause: autoremove has no knowledge of the Brewfile. By passing the Brewfile packages as a protected list, autoremove can make informed decisions rather than being disabled entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions