A new engineer follows the wiki's "Mac setup" page and after two hours has zsh: command not found: brew because the Homebrew installer's final instruction — add eval "$(/opt/homebrew/bin/brew shellenv)" to ~/.zprofile — scrolled past; later, docker-credential-desktop: executable file not found in $PATH appears because a colleague's setup included Docker Desktop and theirs does not. A Brewfile and a short script replace the wiki page with something executable and checkable. This page provisions a macOS developer laptop that way, as part of workstation provisioning and team dotfiles.

The Brewfile covers the workstation baseline only — tools every project assumes. Project-specific versions stay in each repository's toolchain file.

Diagnostic

Check whether Homebrew is installed and on PATH for login shells, and how far the machine differs from the team manifest:

#!/usr/bin/env bash
set -euo pipefail
uname -m
test -x /opt/homebrew/bin/brew && echo "brew installed at /opt/homebrew" || echo "brew not installed"
zsh -lc 'command -v brew' || echo "brew not on PATH for login shells"
if command -v brew >/dev/null; then
  brew bundle check --file=Brewfile --verbose 2>&1 | head -10
fi
/usr/bin/pgrep -q oahd && echo "rosetta installed" || echo "rosetta not installed"

Expected bad output on a half-configured laptop:

arm64
brew installed at /opt/homebrew
brew not on PATH for login shells
rosetta not installed

Homebrew exists but new terminals cannot find it, and Rosetta is missing, so the first cask that ships an Intel binary will stop the installation to prompt.

Why Provisioning Stopped Halfway Decision diagram mapping three common Brewfile provisioning failures to their causes. Why Provisioning Stopped Halfway Where did provisioning stop? brew not found shellenv missing in zprofile Rosetta prompt install Rosetta first cask asks for password needs admin once
PATH, Rosetta and admin prompts cause most interrupted macOS provisioning runs.

Root cause

Homebrew on Apple Silicon installs to /opt/homebrew, which is not on the default PATH, so its installer asks you to add a shellenv line to the login profile — an interactive instruction that is easy to miss and impossible to enforce. Casks that ship Intel-only binaries require Rosetta 2, and if it is absent macOS prompts to install it mid-run. Some casks (VPN clients, virtualisation tools) install privileged helpers and ask for an administrator password, which breaks unattended runs. None of this is visible in a wiki checklist, so each person hits a different subset in a different order, and the resulting machines differ in which tools were installed before the process stalled.

Resolution

  1. Write a small team Brewfile, split into required and optional parts so the required baseline stays short:
# Brewfile — required workstation baseline
brew "git"
brew "gh"
brew "jq"
brew "mise"
brew "chezmoi"
brew "direnv"
brew "mkcert"
brew "colima"
brew "docker"
brew "docker-compose"
brew "docker-buildx"
brew "docker-credential-helper"
cask "visual-studio-code"
cask "1password-cli"
# Brewfile.optional — installed only when asked for
cask "iterm2"
cask "rectangle"
cask "font-jetbrains-mono-nerd-font"
brew "ripgrep"
brew "fd"
  1. Write the install script so it handles Homebrew, PATH, Rosetta and the manifests in the right order, and is safe to re-run:
#!/usr/bin/env bash
set -euo pipefail
if [ "$(uname -m)" = arm64 ] && ! /usr/bin/pgrep -q oahd; then
  sudo softwareupdate --install-rosetta --agree-to-license
fi
if ! command -v brew >/dev/null && [ ! -x /opt/homebrew/bin/brew ]; then
  NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
brew_bin=/opt/homebrew/bin/brew; [ -x "$brew_bin" ] || brew_bin=/usr/local/bin/brew
grep -q 'brew shellenv' ~/.zprofile 2>/dev/null || echo "eval \"\$($brew_bin shellenv)\"" >> ~/.zprofile
eval "$($brew_bin shellenv)"
brew update --quiet
brew bundle --file=Brewfile --no-lock
[ "${WITH_OPTIONAL:-0}" = 1 ] && brew bundle --file=Brewfile.optional --no-lock
brew bundle check --file=Brewfile

NONINTERACTIVE=1 stops the Homebrew installer from waiting for Enter; Rosetta is installed first so no cask stops to prompt; the shellenv line is written idempotently.

  1. Run it once with admin available, since the first run may install casks needing privileges: sudo -v && ./install.sh. Later runs usually need no password.

  2. Schedule a weekly drift check so machines stay aligned with the manifest as it evolves:

#!/usr/bin/env bash
set -euo pipefail
cat > ~/Library/LaunchAgents/dev.acme.brewcheck.plist <<'EOF'
<?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.acme.brewcheck</string>
  <key>ProgramArguments</key><array><string>/bin/zsh</string><string>-lc</string><string>brew bundle check --file=$HOME/src/dotfiles/Brewfile || osascript -e 'display notification "Run install.sh to update tools" with title "Workstation drift"'</string></array>
  <key>StartCalendarInterval</key><dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>10</integer></dict>
</dict></plist>
EOF
launchctl load -w ~/Library/LaunchAgents/dev.acme.brewcheck.plist
Order of the Install Script Ordered steps the macOS install script performs, from Rosetta to a verified bundle. Order of the Install Script 1 — install Rosetta if arm64 2 — install Homebrew non-interactively 3 — write shellenv to zprofile 4 — brew bundle required Brewfile 5 — optional bundle if requested 6 — brew bundle check passes
Doing Rosetta and PATH first removes every mid-run prompt except the first admin password.

Expected output

$ ./install.sh
==> Fetching git, gh, jq, mise, chezmoi, direnv, mkcert, colima, docker ...
Using visual-studio-code
Installing 1password-cli
Homebrew Bundle complete! 14 Brewfile dependencies now installed.
The Brewfile's dependencies are satisfied.
$ zsh -lc 'command -v brew mise docker'
/opt/homebrew/bin/brew
/opt/homebrew/bin/mise
/opt/homebrew/bin/docker

Every baseline tool is installed and on PATH in new login shells, and brew bundle check confirms the machine matches the manifest.

Re-running the script immediately afterwards is a useful check in itself: it should finish in seconds, report every dependency as already satisfied and change nothing. If a second run installs or upgrades something, a step is not idempotent — usually a cask with an auto-updater that reports a version mismatch — and it is better to find that on your own machine than on a new hire's.

Prevention

  1. Test the script on a clean macOS VM (Tart or UTM) for each supported macOS version whenever the Brewfile changes. A failing provisioning script is only discovered on a new hire's first day otherwise.

  2. Keep project tools out of the Brewfile. If a tool needs a specific version for a project, it belongs in that project's mise.toml or devbox.json, as covered in toolchain version management.

  3. Review Brewfile changes like code in the dotfiles repository, so additions are deliberate and removals are announced.

Time to a Working Baseline on a New Mac Bar chart comparing time to a working developer baseline following a wiki page versus running the Brewfile script. Time to a Working Baseline on a New Mac wiki checklist 190 min Brewfile script 24 min
Median across five new hires; most wiki time was lost to PATH and prompt interruptions.

Platform caveats

Apple Silicon (ARM64): Homebrew lives in /opt/homebrew; scripts and dotfiles must not assume /usr/local. Use brew --prefix wherever a path is needed.

macOS (Intel): Homebrew lives in /usr/local and Rosetta is irrelevant; the script's architecture checks handle both.

Managed Macs: MDM may block some casks or require them to come from the company software portal. Keep those out of the Brewfile and list them in the README's "from Self Service" section instead.

Mac App Store apps: mas entries in a Brewfile need the user to be signed in to the App Store; keep them in the optional file to avoid failing unattended runs.

Rollback

brew bundle cleanup lists packages installed outside the manifest, and individual packages can be uninstalled; the LaunchAgent is removed by unloading it:

#!/usr/bin/env bash
set -euo pipefail
launchctl unload -w ~/Library/LaunchAgents/dev.acme.brewcheck.plist
rm -f ~/Library/LaunchAgents/dev.acme.brewcheck.plist
brew bundle cleanup --file=Brewfile

Frequently Asked Questions

Why can new terminals not find brew after installation?

On Apple Silicon, Homebrew lives in /opt/homebrew, which is not on the default PATH. Add eval "$(/opt/homebrew/bin/brew shellenv)" to ~/.zprofile; the install script does this idempotently.

Can a Brewfile pin package versions?

Not in general; Homebrew installs the current version of each formula. That suits workstation tools, which should be current. Anything that must be pinned belongs in a project toolchain file managed by mise, asdf or Devbox.

Should the Brewfile include Docker Desktop?

Only if the organisation has chosen Docker Desktop as its runtime and holds the necessary licences. Otherwise install the Docker CLI and the chosen runtime, such as Colima, as in the example.

How do we handle tools some developers need and others do not?

Keep them in an optional Brewfile or per-role files (Brewfile.data, Brewfile.mobile) and install them with an environment variable or flag, so the required baseline stays small.