Just a reminder for me — what I install on a fresh MacBook Pro. The list used to be a pile of brew install lines I copy-pasted and inevitably let drift; it’s now a single Brewfile that replays the whole machine in one command.

Prerequisites

  • Homebrew:
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  • Git

The Brewfile

brew bundle reads a Brewfile — a manifest of taps, formulae, casks, Mac App Store apps, VS Code extensions and npm globals — and installs everything in it. It ships with Homebrew itself, so the old brew tap homebrew/bundle step is no longer needed.

Creating it

On the machine you’re leaving, snapshot whatever is currently installed:

brew bundle dump --file=Brewfile

That writes a Brewfile in the current directory. The flags worth knowing:

FlagEffect
-f, --forceOverwrite an existing Brewfile instead of refusing
--no-describeDrop the # description comment above each entry (they’re on by default)
--no-vscodeDon’t record VS Code extensions
-g, --globalWrite to ~/.homebrew/Brewfile (or ~/.Brewfile) instead of the current directory

Install mas before dumping if you want App Store apps in the file — brew bundle dump only emits mas lines when the mas CLI is present:

brew install mas

Then commit the result. It’s a snapshot of installed state, it diffs cleanly, and brew bundle dump -f after installing something new keeps it honest.

Anatomy

Mine is here — a straight dump, description comments and all. A representative slice:

tap "go-task/tap"
tap "romaintb/fgj", "https://codeberg.org/romaintb/homebrew-fgj.git"
tap "sgaunet/tools", trusted: true

# Resource monitor. C++ version and continuation of bashtop and bpytop
brew "btop"
# Task is a task runner/build tool that aims to be simpler and easier to use
brew "go-task", link: false
# Official GitLab CI runner
brew "gitlab-runner", restart_service: :changed
# A tool for backing up GitLab projects
brew "sgaunet/tools/gitlab-backup"

# Open-source code editor
cask "visual-studio-code"

vscode "golang.go"
vscode "anthropic.claude-code"

npm "@modelcontextprotocol/server-github"

It’s a Ruby DSL, so # comments and blank lines are free and the ordering doesn’t matter. Six entry types show up in practice:

  • tap — third-party taps, listed first because the brew lines below resolve against them. A second argument gives a custom clone URL when the tap isn’t on GitHub.
  • brew — formulae. Fully-qualified (sgaunet/tools/gitlab-backup) when they come from a tap.
  • cask — GUI apps and fonts.
  • mas — Mac App Store apps, e.g. mas "Amphetamine", id: 937984704. Get the ids with mas list.
  • vscode — VS Code (and forks) extensions.
  • npm — globally installed npm packages.

The trailing options are recorded automatically: link: false for a keg-only-by-choice formula, restart_service: :changed to bounce a service when its formula updates, and trusted: true for third-party taps you’ve run brew trust on — Homebrew keeps that in ~/.homebrew/trust.json and the Brewfile carries it across so a restore doesn’t stop to ask.

Installing from it

On the fresh machine, once Homebrew is in place:

brew bundle --file=Brewfile

install is the default subcommand, so that’s the same as brew bundle install --file=Brewfile. If the file is named Brewfile and sits in the working directory, --file is redundant:

cd ~/dotfiles && brew bundle

Set HOMEBREW_BUNDLE_FILE in ~/.zshrc to point at it from anywhere, or keep it at ~/.homebrew/Brewfile and use brew bundle -g.

Two things it can’t do for you: App Store entries need you signed into the App Store first, and casks for licensed apps (Alfred Powerpack, Moom, Bartender, CleanShot X, Dropzone) install the app but not your licence key.

Keeping it honest

brew bundle check --file=Brewfile    # non-zero exit if anything is missing
brew bundle list  --file=Brewfile    # what the file declares
brew bundle add ripgrep              # append an entry (--cask / --mas / --npm for the rest)
brew bundle cleanup --file=Brewfile  # what's installed but *not* declared

cleanup only reports by default. brew bundle cleanup --force actually uninstalls everything missing from the file — and resets the trust store to whatever the Brewfile declares — which is an efficient way to lose something you’d forgotten to write down. Run the reporting form first.

CLI tools (Homebrew)

The dump above is everything; this is the short list I’d want first on a bare machine:

brew install \
  go-task goreleaser \
  helm k9s kubectx \
  sqlc usql dbmate libpq \
  d2 rclone terraform \
  wget pre-commit osv-scanner

Add libpq to PATH for pg_dump / psql — this bit the Brewfile can’t do for you:

echo 'export PATH="/opt/homebrew/opt/libpq/bin:$PATH"' >> ~/.zshrc

GUI apps (Homebrew Cask)

brew install --cask \
  visual-studio-code \
  firefox \
  docker \
  warp

Mac App Store / direct downloads

I used to install these by hand. Almost all of them turn out to have a cask, so they belong in the Brewfile too:

AppBrewfile entry
Alfredcask "alfred"
Rectanglecask "rectangle"
Moomcask "moom"
AltTabcask "alt-tab"
Bartendercask "bartender"
CheatSheetcask "cheatsheet"
Dropzone 4cask "dropzone"
CleanShot Xcask "cleanshot"
Amphetaminemas "Amphetamine", id: 937984704

The one leftover is Dropover — no cask for it, so it stays an App Store or direct download.