Stop Reinstalling macOS Tools by Hand: Use brew bundle as a Versioned Manifest
Turn Homebrew installs into a versioned Brewfile. Dump, curate, check, and install with brew bundle for reproducible macOS and Linux dev environments.
24 Apr 2026, 07:22 UTC

New machine, same checklist. Install git, install Node, remember the font tap, find the right cask name for VS Code, sign into the Mac App Store for Xcode. The useful takeaway is to stop keeping that list in your head and make it a Brewfile that Homebrew can converge against.
A Brewfile is a plain text manifest for brew bundle. It declares formulae for command-line tools, casks for GUI apps, taps for third-party repositories, and mas lines for Mac App Store apps. Committing it makes environment setup reviewable and repeatable.
Dump first, curate second
brew bundle dump --force writes a Brewfile from what is currently installed. Run it in a terminal where Homebrew is active, typically your user shell on macOS or Linux. It creates a starting point fast, but a raw dump includes transitive dependencies and one-off experiments.
Curate to top-level intent. Keep only packages you would deliberately reinstall. That pruning is the engineering decision: the Brewfile should describe why a tool is needed, not every library it pulls in.
Example Brewfile you can adapt:
# Taps
ap 'homebrew/cask-fonts'
# Command-line formulae
brew 'git'
brew 'node'
# GUI apps via cask
cask 'visual-studio-code'
cask 'font-fira-code-nerd-font'
# Mac App Store, requires mas tool installed separately
mas 'Xcode', id: 497799835
Place this file at the repo root or in your dotfiles. The syntax is declarative; order does not matter, but taps must precede formulae that live in them.
Check as a gate, install to converge
brew bundle check compares the Brewfile to the local Homebrew state and exits non-zero if anything is missing. That exit code makes it usable in onboarding scripts or CI without doing work when the environment is already correct.
brew bundle install reads the Brewfile and installs missing items, skipping those already present. In practice it is idempotent, so rerunning after edits converges the machine without reinstalling everything.
A minimal bootstrap pattern:
brew bundle check || brew bundle install
Run this in a user shell with normal Homebrew permissions. No sudo is required for standard installs. Risks to note: mas entries need an active App Store sign-in and are region dependent, so they frequently fail in headless or CI contexts. Keep them optional or document the manual step.
Cleanup is a scalpel, not a broom
brew bundle cleanup lists installed packages not present in the Brewfile. With --force it removes them. That is powerful for personal machines but risky on shared workstations where teammates install ad-hoc tools.
Always run cleanup without --force first to review the list. Verify flag names with brew bundle --help and brew bundle dump --help on your installed Homebrew version, because subcommand flags evolve across releases.
Limitations to plan for
A dumped Brewfile captures state, not intent. Review before committing. mas is not automation friendly. Cleanup can uninstall tools deliberately kept outside the manifest. Brewfile semantics change between Homebrew releases, so pin your documentation to the version you test against.
Practical verification: create a minimal Brewfile with one formula, run brew bundle check before and after brew bundle install, and observe the exit status change. Run brew bundle cleanup without --force on a test machine to see what would be removed before trusting it in automation.
Commit the curated Brewfile, add the check to your setup script, and new-machine setup becomes one command with a reviewable history.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.