Skip to content

brew bundle cleanup uninstalls Mac App Store apps that were never installed via Homebrew #22450

@jcf

Description

@jcf

brew doctor output

Your system is ready to brew.

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.1.14
ORIGIN: https://github.com/Homebrew/brew
HEAD: 10a163ac127624caa80cc5cc5a705e97f3615b0e
Last commit: 5 days ago
Branch: stable
Core tap JSON: 29 May 12:34 UTC
Core cask tap JSON: 29 May 12:34 UTC
HOMEBREW_PREFIX: /opt/homebrew
HOMEBREW_BROWSER: open
HOMEBREW_BUNDLE_USER_CACHE: /Users/jcf/.cache/bundle
HOMEBREW_CASK_OPTS: []
HOMEBREW_DISPLAY: max
HOMEBREW_DOWNLOAD_CONCURRENCY: 32
HOMEBREW_EDITOR: emacsclient --tty
HOMEBREW_FORBID_PACKAGES_FROM_PATHS: set
HOMEBREW_MAKE_JOBS: 16
HOMEBREW_NO_ENV_HINTS: set
Homebrew Ruby: 4.0.5 => /opt/homebrew/Library/Homebrew/vendor/portable-ruby/4.0.5_1/bin/ruby
CPU: 16-core 64-bit arm_brava
Clang: 21.0.0 build 2100
Git: 2.50.1 => /Applications/Xcode.app/Contents/Developer/usr/bin/git
Curl: 8.7.1 => /usr/bin/curl
macOS: 26.5-arm64
CLT: 26.5.0.0.1777544298
Xcode: 26.5
Metal Toolchain: N/A
Rosetta 2: false

What were you trying to do (and why)?

Maintain a small Brewfile with mas entries for the App Store apps I want Homebrew to install on a fresh setup, alongside formulae, casks, and taps. The Brewfile is generated from a Nix expression by nix-darwin's homebrew module, but the resulting Brewfile is unremarkable — mas "Tailscale", id: 1475387142 and similar lines for a handful of apps.

I expect brew bundle install --cleanup to remove things Homebrew installed but that are no longer in the Brewfile. I do not expect it to remove App Store apps that I installed by clicking "Get" in the App Store app years ago, and which Homebrew has never had any relationship with.

What happened (include all command output)?

Since #22395 (bundle: extend cleanup coverage, merged 2026-05-24), brew bundle cleanup uninstalls every Mac App Store app on the system that is not listed in the Brewfile, regardless of how the app got installed. The cleanup logic in bundle/extensions/mac_app_store.rb has no provenance tracking — it simply shells out to mas list, subtracts the Brewfile's mas entries, and runs mas uninstall on the remainder.

mas list reports every App Store app on the machine, regardless of how it was installed (App Store UI, mas install, Apple Configurator, MDM push). Homebrew cannot distinguish "installed via my Brewfile" from "installed via the App Store UI five years ago" — they look identical. Once the Brewfile has any mas line in it, every other App Store app becomes a cleanup candidate.

On my machine, a single darwin-rebuild switch after brew update self-updated to a post-#22395 version uninstalled these App Store apps with no warning, despite none of them being in the Brewfile and none of them having ever been installed via mas or Homebrew:

  • Darkroom
  • Screens 5
  • Infuse
  • JSON Peep (Safari extension)
  • Pixelmator Pro
  • Noir (Safari extension)
  • Xcode

These had been installed years ago via the App Store UI. They had nothing to do with Homebrew, were never listed in any Brewfile, and brew had never installed or upgraded them.

#22439 (merged 2026-05-28) patched one subset of the damage by skipping TestFlight apps (which mas list reports with id=0). That fix only addresses TestFlight; the broader case — App Store apps installed via the App Store UI — remains in place by design.

What did you expect to happen?

brew bundle cleanup should only uninstall MAS apps that Homebrew installed via a mas Brewfile entry. Apps installed via the App Store UI, MDM, or any non-Homebrew path should be left alone, exactly as they were prior to #22395.

If maintaining one-to-one parity between a Brewfile and mas list is a feature some users want, it should be opt-in (brew bundle cleanup --mas or --all invoked explicitly), not the default of brew bundle install --cleanup / --zap.

Currently the default makes it unsafe to have any mas line in a Brewfile unless the Brewfile is exhaustive — i.e. unless the user has declared every single App Store app they own. Most users do not maintain exhaustive Brewfiles for their personal Apple ID purchase history. The result is a silent, destructive default that surprised users and could not have been anticipated from the PR description (which does not flag any breaking-change risk for users with existing mas entries).

Three options, in order of decreasing strictness:

  1. Revert MAS cleanup to its pre-bundle: extend cleanup coverage #22395 behaviour. Treat App Store apps as out-of-scope for brew bundle cleanup entirely. This is the safest default and matches the principle that a package manager should not delete software it didn't install.
  2. Gate MAS cleanup behind an explicit opt-in flag that is not implied by --cleanup or --zap. For example, only run MAS cleanup when --mas or --all is passed. Plain brew bundle install --cleanup should not include mas in its scope.
  3. At minimum, if the current default is retained, add a prominent release-note callout for the version that shipped bundle: extend cleanup coverage #22395, and have brew bundle install --cleanup print a one-time warning the first time it identifies MAS apps it would remove that were not installed by mas.

Downstream tools that wrap brew bundle (notably nix-darwin's homebrew module, which sets cleanup = "zap" by user choice and runs brew bundle install --cleanup --zap non-interactively during system activation) silently inherited the new destructive behaviour. Users of those tools had no opportunity to consent to the change.

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

# Prerequisite: at least one App Store app installed via the App Store UI
# that is NOT a TestFlight build (so `mas list` reports a positive id).
# e.g. Pages, Numbers, Xcode — anything bought with your Apple ID.

mas list
#  409201541  Pages       (14.3)
#  497799835  Xcode       (26.5)
#  ...

cat > /tmp/Brewfile <<'EOF'
mas "Tailscale", id: 1475387142
EOF

brew bundle cleanup --file=/tmp/Brewfile
# Would uninstall Mac App Store apps:
# Pages (409201541)
# Xcode (497799835)
# ...
# Run `brew bundle cleanup --force` to make these changes.

# Every App Store app the user has, except Tailscale, is now listed
# as a cleanup candidate — even though `brew`/`mas` did not install
# any of them.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions