brew-export: Pack Your Entire Mac Environment Into a Tarball
Every time I get a new Mac, the same routine plays out. Open a terminal. Try to remember what I had installed. Run brew install a dozen times. Wonder why something is broken and eventually realize I'm missing a tap. Dig through dotfiles manually. It's tedious and I always miss something.
So I wrote brew-export — a single shell script that snapshots your entire Homebrew environment and selected dotfiles into a portable tarball. Drop it on a new machine, run one script, done.
What it does
brew_export.sh produces a <name>.tar.gz containing three things:
- A
Brewfile— generated bybrew bundle dump, capturing every formula, cask, tap, and Mac App Store app - A self-contained install script that handles the full restore sequence
- Any dotfiles you selected during export
The tarball is self-contained. There's no dependency on the export machine once it's built.
The install sequence
The generated install script walks through five steps automatically:
Step 1 — Homebrew. Checks if it's installed. If not, runs the official install script and handles the Apple Silicon shellenv path so brew is available immediately without opening a new shell.
Step 2 — Custom taps. Any taps outside of homebrew/core and homebrew/cask are captured at export time and baked into the install script as explicit brew tap calls. This is the part that always bit me before — packages from third-party taps fail silently if the tap isn't registered first.
Step 3 — Mac App Store. If there are MAS apps in the Brewfile, the script checks for mas and installs it automatically if it's missing. Then it checks App Store sign-in. If you're not signed in, it installs everything else, exits cleanly, and tells you exactly what to do next. Re-run after signing in and it picks up where it left off.
Step 4 — brew bundle install. Runs against the Brewfile. By this point taps are registered and MAS is ready, so it goes cleanly.
Step 5 — Dotfiles. Copies selected dotfiles back to ~. If a file already exists, you're prompted per file — overwrite, skip, or backup-and-overwrite. Backups get a timestamp suffix so nothing is lost.
Dotfile selection
During export, the script scans ~ for top-level dotfiles and ~/.ssh for any files. If fzf is installed, you get a TAB-based multi-select interface. If not, it falls back to a numbered checklist.
📝 Select dotfiles to include (TAB to multi-select, ENTER to confirm):
dotfiles > /Users/zackwag/.gitconfig
/Users/zackwag/.zshrc
/Users/zackwag/.zshenv
/Users/zackwag/.ssh/config
/Users/zackwag/.ssh/id_ed25519
SSH files get a warning. If you include private keys, the tarball needs to be stored and transferred securely — the script reminds you of that at the end.
Usage
# Defaults to your hostname as the filename base
./brew_export.sh
# Or specify a name
./brew_export.sh my-macbook-setup
This produces my-macbook-setup.tar.gz. The tarball layout:
my-macbook-setup/
├── my-macbook-setup.Brewfile
├── my-macbook-setup_install.sh
└── dotfiles/
├── .gitconfig
├── .zshrc
└── .ssh/
└── config
On the new machine:
tar -xzf my-macbook-setup.tar.gz
cd my-macbook-setup
bash my-macbook-setup_install.sh
A few design decisions worth explaining
sync after the Brewfile dump. brew bundle dump returns before the filesystem has necessarily flushed. On fast machines this isn't usually a problem, but the MAS detection reads the Brewfile immediately after the dump. A sync call makes that safe.
mas is installed automatically. If your Brewfile has App Store apps, the export script installs mas for you if it isn't already present. No prerequisite steps.
mapfile vs read -a. macOS ships bash 3.2. mapfile is bash 4+. The fzf selection result gets read into an array using IFS=$'\n' read -r -d '' -a instead, which works on stock macOS without requiring a newer bash.
Banner alignment. Terminal emoji are double-width characters — they occupy 2 columns but ${#string} counts them as 1. Every box border was off by one per emoji. Fixed by measuring true display width using unicodedata.east_asian_width() in Python and computing correct padding before hardcoding it into the script.
The code
Available on GitHub at zackwag/brew-export.