devAlice
← Mac

Brewfile Management — Reproduce a Mac Dev Environment as Code with `brew bundle`

A Brewfile pattern that pins your Mac setup into a single file. dump → cleanup → cleanup → install on another machine, one full cycle.

Typing brew install line by line every time you set up a new Mac is inefficient. brew bundle lets a single Brewfile bundle brew formulae, casks, VS Code extensions, and even App Store apps into something reproducible. Once you internalize the pattern, machine swaps and teammate onboarding shrink by 30‑minute increments.

This guide targets macOS 14+ / Homebrew 4.x. It is the freeze-your-environment-as-code step after Mac initial setup.

TL;DR

  1. Use brew bundle dump --describe to snapshot the current state into a Brewfile
  2. Hand‑edit (remove duplicates and test packages) → commit to Git
  3. On a new Mac: brew bundle install --file=./Brewfile → done
  4. Periodically brew bundle cleanup to drop anything not in the Brewfile

Prerequisites

  • Homebrew 4.x installed — see Mac initial setup
  • (Optional) A GitHub repo or Gist (to store the Brewfile)
  • (Optional) mas-clibrew install mas if you want to bundle Mac App Store apps too

Download an Example Brewfile

The devAlice recommended baseline — grab it and edit to suit your environment.

Brewfile
# 1. Download
curl -fsSL https://devalice.jaceclub.com/assets/mac/brewfile-management/Brewfile.example -o Brewfile
 
# 2. Verify SHA-256
shasum -a 256 Brewfile
# Expected: 2b54214d29391d15666fe5e15768aa2e6e6eabea8b3c772989d6e1216a816c5f
 
# 3. Inspect (review which packages are in it; edit to fit your environment)
less Brewfile
 
# 4. Apply
brew bundle install --file=./Brewfile

1. Brewfile Syntax Basics — 5 min

It's Ruby DSL under the hood, but you really only need five keywords.

# Brewfile
 
# tap — extra repository
tap "homebrew/bundle"
tap "homebrew/services"
 
# brew — formulae (CLI tools)
brew "git"
brew "gh"
brew "mise"
 
# Optional arguments are allowed
brew "postgresql@16", restart_service: :changed
brew "redis",         start_service: true
 
# cask — GUI apps
cask "visual-studio-code"
cask "rectangle"
 
# Mac App Store (requires mas-cli)
mas "Xcode", id: 497799835
 
# VS Code extensions (similar syntax to mas)
vscode "dbaeumer.vscode-eslint"
vscode "esbenp.prettier-vscode"

Comments + Sectioning

# ─── CLI ──────────────────────────────────────
brew "git"
brew "gh"
 
# ─── GUI ──────────────────────────────────────
cask "visual-studio-code"

Section separators pay back hugely in maintenance — diffs become meaningful after each commit.


2. Generate a Brewfile from Current State — dump — 3 min

If you already have a well-set-up machine, snapshot it in one shot.

# Create a Brewfile in the current directory (--force overwrites)
brew bundle dump --force --describe
 
# --describe adds a # comment to every line — readability ↑

Sample output:

# ./Brewfile (auto-generated)
tap "homebrew/services"
brew "git"
# Distributed revision control system
brew "gh"
# GitHub command-line tool
brew "jq"
# Lightweight and flexible command-line JSON processor
brew "mise"
# Polyglot runtime manager (asdf rust clone)
cask "visual-studio-code"
# Open-source code editor

Clean Up the Dump by Hand

A fresh dump captures everything currently installed, which usually has noise. Cleanup checklist:

  1. Test-only packages you no longer use → remove
  2. Language runtimes → drop them if you'll manage them with mise (e.g., node, python@3.x)
  3. Work vs personal → split into Brewfile.work / Brewfile.personal
  4. Add section comments

A 10-minute investment now saves an hour of future setup.


3. Apply on Another Machine — install — 10–30 min

In the directory containing your Brewfile, on the new machine (or after cleanup):

brew bundle install --file=./Brewfile
 
# Or use an absolute path
brew bundle install --file=$HOME/dotfiles/Brewfile
 
# Already-installed items are skipped (idempotent)

The first run is dominated by cask downloads (~50–200MB each). Plan for 10–30 minutes.

Step-by-Step View — --verbose

brew bundle install --file=./Brewfile --verbose

Failures Only — --quiet

brew bundle install --file=./Brewfile --quiet

4. Drop Packages Not in the Brewfile — cleanup — 5 min

To make your Brewfile the single source of truth, remove anything not listed.

# Dry run — see what would be removed
brew bundle cleanup --file=./Brewfile
 
# Actually remove — be deliberate
brew bundle cleanup --file=./Brewfile --force

⚠️ --force uninstalls immediately. Only run it after your Brewfile is fully cleaned; otherwise you might need to reinstall.


5. Split Strategy — work / personal / experiment

A single monolithic Brewfile eventually bloats. Split by context.

~/dotfiles/
├── Brewfile           # Common to all (CLI essentials)
├── Brewfile.work      # Work-only (Slack, Zoom, ...)
├── Brewfile.personal  # Personal (game launchers, ...)
└── Brewfile.optional  # Occasionally used

Apply Them Together

cd ~/dotfiles
brew bundle install --file=./Brewfile
brew bundle install --file=./Brewfile.work
brew bundle install --file=./Brewfile.personal

Or via Makefile/shell script

# ~/dotfiles/bundle.sh
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
for f in Brewfile Brewfile.work Brewfile.personal; do
  [ -f "$f" ] && brew bundle install --file="./$f"
done
echo "✓ All Brewfiles applied"

6. Version Control with Git — 3 min

A Brewfile is a core file in any dotfiles repo.

cd ~/dotfiles
git init
git add Brewfile Brewfile.*
git commit -m "feat: initial Brewfile baseline"
git remote add origin git@github.com:<user>/dotfiles.git
git push -u origin main

When adding/removing packages later:

brew bundle dump --force --describe   # refresh
git diff Brewfile                     # review changes
git commit -am "feat: add postgresql + redis"
git push

On a new machine:

git clone git@github.com:<user>/dotfiles.git ~/dotfiles
cd ~/dotfiles
brew bundle install --file=./Brewfile

7. Automation — brew services + macOS LaunchAgent

Auto-dump once a day so your brew state doesn't drift.

~/Library/LaunchAgents/dev.local.brewdump.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>dev.local.brewdump</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>-lc</string>
    <string>cd $HOME/dotfiles && /opt/homebrew/bin/brew bundle dump --force --describe</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict><key>Hour</key><integer>22</integer><key>Minute</key><integer>0</integer></dict>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/dev.local.brewdump.plist

Auto-dumps at 22:00 every day. Each morning, git diff shows what changed overnight.


8. Troubleshooting

"Error: Cask 'X' is not installed" (during cleanup)

The cask was already removed elsewhere. Run brew bundle cleanup --force again to reconcile.

Dump captures too many unintended packages

Old transitive dependencies showing up as top-level. Tidy them up:

brew autoremove   # remove leaf packages only used as deps
brew bundle dump --force --describe  # re-dump

App Store apps don't appear in the dump

mas-cli is missing. brew install mas and re-dump.

Some casks fail SHA-256 during install

Happens right after Homebrew updates cask definitions. brew update and retry.


Next Steps

References

Changelog

  • 2026-05-12 — Initial draft (devAlice M2 seed expansion)

Comments