cheat sheet
Homebrew
Install, search, upgrade, pin, tap, bottle, and bundle packages with Homebrew on macOS: formulae vs casks, brew services, Brewfile-driven machine bootstrap, Apple-Silicon vs Intel prefixes, brew doctor, brew cleanup, and recovery recipes.
Homebrew — The Missing Package Manager for macOS
What it is
Homebrew is the de-facto third-party package manager for macOS (and a first-class citizen on Linux as Linuxbrew), maintained by an open-source community led by Mike McQuaid and written in Ruby. It installs CLI utilities as formulae (compiled from source or — more commonly — fetched as a pre-built bottle) and GUI applications as casks (drag-installed .app bundles or .pkg installers managed under the hood). Reach for brew when you want a single, reproducible mechanism to install, upgrade, pin, and remove software on macOS — the rough equivalent of apt on Debian, winget on Windows, or dnf on Fedora. Alternatives to be aware of: MacPorts (older, builds-from-source by default, no casks) and nix-darwin (declarative, immutable, much steeper learning curve).
Install
Homebrew installs itself with a one-liner that downloads the official installer script over HTTPS, runs it under your user account, and writes everything to a prefix directory (/opt/homebrew on Apple Silicon, /usr/local on Intel Macs). The installer sets up the directory layout, creates the brew symlink, and prints a final block of eval "$(/opt/homebrew/bin/brew shellenv)" instructions you must paste into your shell profile.
# Standard installer (Apple Silicon and Intel)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Wire brew into the current shell (run once; also add to ~/.zprofile)
eval "$(/opt/homebrew/bin/brew shellenv)"
# Verify
brew --version
brew --prefix
Output:
Homebrew 4.6.0
Homebrew/homebrew-core (git revision a1b2c3d; last commit 2026-05-25)
/opt/homebrew
Homebrew has moved fast in the 4.x line: 4.4 added official macOS Sequoia (15) support and INSTALL_RECEIPT.json for casks; 4.5 made brew bundle and brew services built-in commands (no external tap), bumped to Portable Ruby 3.4.3, and enabled Bootsnap by default; 4.6 added preliminary macOS 26 Tahoe support, concurrent downloads via HOMEBREW_DOWNLOAD_CONCURRENCY, the new brew mcp-server (a Model Context Protocol server for AI agents), and switched every Homebrew repository's default branch to main.
To uninstall Homebrew entirely (formulae, casks, prefix and all), use the official uninstaller script:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
Output: (none — exits 0 on success)
Syntax
Every brew invocation follows a brew <command> [subcommand] [options] [arg...] shape. Commands fall into a handful of buckets: query (info, search, list, outdated, deps), mutate (install, upgrade, uninstall, pin, link, unlink), tap and source management (tap, untap, update), service control (services), bundle (bundle), and diagnostics (doctor, config, cleanup).
brew <command> [<subcommand>] [<flags>] [<formula-or-cask>...]
Output: (none — exits 0 on success)
Essential options
| Flag | Meaning |
|---|---|
--cask | Operate on a cask (GUI app) rather than a formula |
--formula | Force formula-mode when a name exists in both namespaces |
--HEAD | Install the upstream HEAD/main branch instead of the latest stable |
--build-from-source, -s | Compile locally even if a bottle is available |
--force-bottle | The opposite — refuse to fall back to source |
--quiet, -q | Suppress informational output |
--verbose, -v | Show downloads, build steps, and post-install scripts |
--dry-run, -n | Print what would happen without changing anything (most subcommands) |
--debug, -d | Drop into an interactive debugger on failure |
--force | Override safety checks (use sparingly) |
--ignore-dependencies | Skip dependency resolution (useful for surgical fixes) |
--only-dependencies | Install dependencies but skip the target itself |
--ask | (4.5+) Preview packages, dependencies, and download sizes before confirming the install |
--skip-link | (4.5+) Install into the Cellar but don't symlink into bin/; pair with brew link later |
--as-dependency | (4.5+) Mark the formula as a dependency rather than user-requested (won't appear in brew leaves) |
Formulae
A formula is Homebrew's recipe for a CLI tool — a Ruby class checked into a tap (a git repository of formulae) that declares the source URL, checksum, dependencies, build steps, and post-install caveats. brew install <name> resolves a formula by name across the configured taps and either downloads its pre-built bottle (a tar.gz of the compiled artefact, signed by the Homebrew project) or compiles it from source if no bottle exists for your macOS version and architecture.
# Install a CLI utility
brew install ripgrep
# Install multiple at once
brew install ripgrep fd bat eza fzf
# Install a specific version (requires a versioned formula)
brew install node@20
# Install from a local formula file
brew install ./my-formula.rb
# Install only the dependencies of a formula (useful when bisecting a broken tool)
brew install --only-dependencies ffmpeg
# Build from source instead of using a bottle
brew install --build-from-source ripgrep
# Preview before committing (4.5+) — prints packages, deps, and download size
brew install --ask ffmpeg
# Install but don't link into bin/ (4.5+) — useful for parallel versioned formulae
brew install --skip-link node@20
# Mark as a dependency, not a user request (4.5+) — keeps `brew leaves` honest
brew install --as-dependency openssl@3
Output:
==> Fetching dependencies for ripgrep: pcre2
==> Downloading https://ghcr.io/v2/homebrew/core/pcre2/manifests/10.43
==> Fetching ripgrep
==> Downloading https://ghcr.io/v2/homebrew/core/ripgrep/manifests/14.1.1
==> Pouring ripgrep--14.1.1.arm64_sonoma.bottle.tar.gz
==> Caveats
zsh completions have been installed to:
/opt/homebrew/share/zsh/site-functions
==> Summary
🍺 /opt/homebrew/Cellar/ripgrep/14.1.1: 19 files, 6.2MB
Show what's installed and where:
brew list
brew list --formula
brew list --versions
brew list --cask
Output:
ripgrep 14.1.1
fd 10.1.0
bat 0.24.0
eza 0.18.21
fzf 0.53.0
node 22.2.0
node@20 20.13.1
Casks
A cask is Homebrew's recipe for a graphical app or other binary distribution — a Ruby class in homebrew/cask (or any tap) that declares a download URL, checksum, and the install style (.app, .pkg, .dmg, font, suite, screensaver, etc.). Casks land under /Applications (or ~/Applications if you pass --appdir) and are tracked in $(brew --prefix)/Caskroom. Reach for casks instead of dragging .dmg files manually so you get versioning, easy uninstall, and auto-upgrade.
brew install --cask firefox
brew install --cask visual-studio-code
brew install --cask docker
# Install to a custom directory
brew install --cask --appdir=~/Applications iterm2
# List installed casks
brew list --cask
# Show info for a cask
brew info --cask firefox
# Uninstall (deletes app bundle and support files Homebrew knows about)
brew uninstall --cask firefox
# "Zap" — also delete user preferences, caches, and Application Support
brew uninstall --cask --zap firefox
Output:
==> Downloading https://download-installer.cdn.mozilla.net/pub/firefox/releases/126.0/mac/en-US/Firefox%20126.0.dmg
==> Installing Cask firefox
==> Moving App 'Firefox.app' to '/Applications/Firefox.app'
🍺 firefox was successfully installed!
Caveat: a cask whose download URL is not pinned will refuse to update when the upstream artefact changes hash. Re-install with --no-quarantine to skip the Gatekeeper attribute if Homebrew complains the binary is untrusted.
brew install --cask --no-quarantine my-unsigned-tool
Output: (none — exits 0 on success)
Search and info
brew search queries the configured taps for formula and cask names matching a string or regex, and brew info prints everything Homebrew knows about a package — version, dependencies, bottle availability, install path, conflicting formulae, post-install caveats, and JSON-flavoured metadata when --json=v2 is passed.
# Free-text search
brew search node
brew search "/^post.*sql$/" # regex
# Cask-only search
brew search --cask "/^docker/"
# Info, with the long-form caveats
brew info ripgrep
brew info --cask firefox
# Machine-readable
brew info --json=v2 ripgrep | jq '.formulae[0].versions'
Output:
==> Formulae
node node@18 node@20 node@22 node-build nodebrew nodenv
==> Casks
node-installer
==> ripgrep: stable 14.1.1 (bottled), HEAD
Search tool like grep and The Silver Searcher
https://github.com/BurntSushi/ripgrep
Installed
/opt/homebrew/Cellar/ripgrep/14.1.1 (19 files, 6.2MB) *
Poured from bottle using the formulae.brew.sh API on 2026-05-24 at 09:14:02
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/r/ripgrep.rb
==> Dependencies
Build: rust ✘
Required: pcre2 ✔
==> Caveats
Bash completion has been installed to:
/opt/homebrew/etc/bash_completion.d
==> Analytics
install: 312,890 (30 days), 1,084,512 (90 days), 4,201,668 (365 days)
Upgrade and maintenance
The standard maintenance loop is brew update (refresh the local clone of every tap so Homebrew sees the newest formula definitions) → brew outdated (list packages that have newer versions available) → brew upgrade (download and install the newer versions) → brew cleanup (delete cached downloads and outdated kegs). Run it weekly or wire it into a launch agent so a fresh shell always has up-to-date tooling.
# Refresh formula definitions from every tap
brew update
# Faster alternative (4.5+) — only fetches if the local clone is older than the auto-update window
brew update-if-needed
# Show what's stale
brew outdated
brew outdated --cask
brew outdated --greedy # include casks that don't have version strings
# Upgrade everything
brew upgrade
# Upgrade a single package
brew upgrade ripgrep
# Upgrade casks (use --greedy if a cask reports its version as "latest")
brew upgrade --cask
brew upgrade --cask --greedy
# Dry-run
brew upgrade --dry-run
Output:
==> Updated Homebrew
Updated 4 taps (homebrew/core, homebrew/cask, hashicorp/tap, mongodb/brew).
==> New Formulae
zellij tokei-rs ast-grep typst
==> Outdated Formulae
node 22.1.0 -> 22.2.0
ripgrep 14.1.0 -> 14.1.1
The cleanup subcommand reclaims disk space:
# Remove old downloads and outdated installed versions
brew cleanup
# Be aggressive — also clear the download cache and prune dead symlinks
brew cleanup --prune=all -s
# See what would be removed (no changes applied)
brew cleanup --dry-run
Output:
Removing: /Users/alice/Library/Caches/Homebrew/downloads/...node--22.1.0...bottle.tar.gz... (45MB)
Removing: /opt/homebrew/Cellar/node/22.1.0... (61MB)
Pruned 0 symbolic links and 3 directories from /opt/homebrew
==> This operation has freed approximately 106MB of disk space.
brew services
brew services is Homebrew's wrapper around macOS launchd (and systemd on Linux) — it generates a launch agent plist for any formula that ships service metadata, installs the plist to ~/Library/LaunchAgents, and starts/stops/restarts it. Use it instead of hand-authoring plists for the dozen-or-so daemons (Postgres, Redis, nginx, MongoDB, Memcached, Mosquitto, etc.) that Homebrew formulae ship with service do … blocks.
# List all known services and their state
brew services list
# Start, stop, restart
brew services start postgresql@16
brew services stop postgresql@16
brew services restart postgresql@16
# Run once without registering for autostart
brew services run postgresql@16
# Run as root (system-wide LaunchDaemon under /Library/LaunchDaemons)
sudo brew services start nginx
# Remove the launch agent entirely
brew services cleanup
Output:
Name Status User File
mongodb-community started alice ~/Library/LaunchAgents/homebrew.mxcl.mongodb-community.plist
postgresql@16 started alice ~/Library/LaunchAgents/homebrew.mxcl.postgresql@16.plist
redis none
nginx error 256 alice ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist
When a service shows an error code, peek at the launch agent's stdout/stderr — Homebrew writes both to $(brew --prefix)/var/log/<formula>.log.
tail -F "$(brew --prefix)/var/log/nginx.log"
Output: (none — exits 0 on success)
Brewfile and brew bundle
brew bundle is Homebrew's manifest-driven installer — point it at a Brewfile listing tap, brew, cask, mas (Mac App Store), whalebrew, and vscode entries, and it installs (or removes) everything to match the file. A Brewfile lives at the project root or in ~/.Brewfile; check it into git so any team-mate or replacement machine can re-create the exact same toolchain with a single command. This is the macOS analog of Pipfile, requirements.txt, or winget configure.
A representative Brewfile:
# ~/Brewfile
tap "homebrew/cask-fonts"
tap "hashicorp/tap"
# CLI essentials
brew "ripgrep"
brew "fd"
brew "bat"
brew "eza"
brew "fzf"
brew "jq"
brew "yq"
brew "gh"
brew "git-delta"
brew "tldr"
brew "tree"
# Languages
brew "node@22"
brew "python@3.12"
brew "pyenv"
brew "uv"
brew "rustup"
# Databases
brew "postgresql@16", restart_service: :changed
brew "redis", restart_service: true
# Cloud / infra
brew "hashicorp/tap/terraform"
brew "awscli"
brew "kubernetes-cli"
# Casks (GUI apps)
cask "firefox"
cask "visual-studio-code"
cask "iterm2"
cask "rectangle"
cask "raycast"
cask "docker"
# Fonts
cask "font-jetbrains-mono-nerd-font"
# Mac App Store apps via `mas`
mas "Things 3", id: 904280696
mas "Xcode", id: 497799835
# VS Code extensions
vscode "GitHub.copilot"
vscode "esbenp.prettier-vscode"
Apply, audit, and dump commands:
# Install everything in ./Brewfile (or pass --file= for a custom path)
brew bundle install
# Use a Brewfile in your home directory
brew bundle --global
# Show what's installed but missing from the Brewfile
brew bundle cleanup --dry-run
# Actually uninstall anything not listed
brew bundle cleanup --force
# Dump the current machine to a Brewfile
brew bundle dump --force --describe
brew bundle dump --file ~/dotfiles/Brewfile --force
# Check if the current machine matches the Brewfile (CI-friendly)
brew bundle check
Output:
Using homebrew/cask-fonts
Using hashicorp/tap
Using ripgrep
Installing fd
Installing bat
Using node@22
Using python@3.12
Installing rectangle
Homebrew Bundle complete! 26 Brewfile dependencies now installed.
Taps
A tap is a third-party formula repository that extends the default homebrew/core and homebrew/cask. Taps are git URLs cloned under $(brew --prefix)/Library/Taps; once tapped, formulae in that repo are searchable and installable as tap-name/formula. Use taps for company-internal formulae, vendor tools (hashicorp/tap, mongodb/brew, cloudflare/cloudflare), or experimental forks.
# Tap a public repo
brew tap hashicorp/tap
brew tap mongodb/brew
brew tap cloudflare/cloudflare
# Tap a private repo (SSH URL)
brew tap mycompany/internal git@github.com:mycompany/homebrew-internal.git
# Install a formula from a specific tap
brew install hashicorp/tap/terraform
brew install mongodb/brew/mongodb-community
# List the active taps
brew tap
# Show formulae provided by a tap
brew tap-info hashicorp/tap
brew search --formula hashicorp/tap/
# Untap (removes the local clone; installed formulae stay put)
brew untap hashicorp/tap
Output:
hashicorp/tap
homebrew/bundle
homebrew/cask
homebrew/cask-fonts
homebrew/core
homebrew/services
mongodb/brew
To author your own tap, the directory layout is the same as homebrew/core: Formula/<name>.rb for CLI tools and Casks/<name>.rb for GUI apps. Push the repo to GitHub as homebrew-<tap-name> and anyone can brew tap user/tap-name.
Pinning, linking, and switching versions
Sometimes you need to lock a formula at a known-good version even when brew upgrade runs against the rest of the fleet — that's brew pin. The companion tools brew link/brew unlink/brew switch move symlinks around in $(brew --prefix)/bin so multiple versioned formulae (node@18, node@20, node@22) can co-exist with only one active at a time.
# Pin a formula so `brew upgrade` skips it
brew pin node@22
# Show what's pinned
brew list --pinned
# Unpin to resume normal upgrades
brew unpin node@22
# Symlink a formula into bin/ (default after install)
brew link node@20
# Use a specific versioned formula instead of the default
brew unlink node && brew link --overwrite --force node@20
# Force when symlinks clash (e.g. competing python@ formulae)
brew link --force python@3.12
Output:
Linking /opt/homebrew/Cellar/node@20/20.13.1...
17 symlinks created.
Diagnostics — brew doctor and brew config
brew doctor is Homebrew's self-check — it scans for the dozen-or-so common breakage patterns (stale symlinks, conflicting /usr/local installs, broken Ruby gems, outdated Xcode CLI tools, PATH disorder, unexpected directories under the prefix) and prints actionable warnings. Pair it with brew config (machine and toolchain inventory) and brew --env (the environment Homebrew exports during builds) when filing an issue.
brew doctor
brew config
brew --env
brew missing # formulae installed without their deps satisfied
Output (brew doctor — clean machine):
Your system is ready to brew.
Output (brew doctor — typical warnings):
Warning: You have unlinked kegs in your Cellar.
Leaving kegs unlinked can lead to build-trouble and cause formulae that depend on those kegs to fail to run properly once built.
Run `brew link` on these:
python@3.11
Warning: Some installed formulae are deprecated or disabled.
You should find replacements for the following formulae:
homebrew/cask/font-meslo-for-powerline
Warning: Homebrew/homebrew-core was not tapped via HTTPS.
Please check whether you have a `git@` URL configured.
==> Configuration
HOMEBREW_VERSION: 4.6.0
ORIGIN: https://github.com/Homebrew/brew
HEAD: abc123def
Last commit: 6 hours ago
Core tap origin: https://github.com/Homebrew/homebrew-core
Core tap HEAD: 9f8e7d6
Core tap last commit: 47 minutes ago
HOMEBREW_PREFIX: /opt/homebrew
HOMEBREW_CASK_OPTS: []
HOMEBREW_MAKE_JOBS: 10
Homebrew Ruby: 3.4.3 => /opt/homebrew/Library/Homebrew/vendor/portable-ruby/3.4.3/bin/ruby
CPU: dodeca-core 64-bit arm_apple_m2_pro
Clang: 15.0.0 build 1500
Git: 2.45.1 => /opt/homebrew/bin/git
Curl: 8.7.1 => /usr/bin/curl
macOS: 26.5-arm64
CLT: 16.0.0.0.1.1715000000
Xcode: N/A
Rosetta 2: false
Concurrent downloads and the MCP server
Homebrew 4.6 added two notable productivity features. HOMEBREW_DOWNLOAD_CONCURRENCY enables parallel bottle downloads (a big win when you brew install a long list at once or brew upgrade after weeks away); setting it to auto lets Homebrew pick a sensible value based on your CPU and network. brew mcp-server ships a built-in Model Context Protocol server so AI coding agents (Claude Code, Codex, Cursor) can search, install, and query Homebrew without shelling out to a brittle wrapper script.
# One-off (only this invocation)
HOMEBREW_DOWNLOAD_CONCURRENCY=auto brew upgrade
# Persist in your shell profile
echo 'export HOMEBREW_DOWNLOAD_CONCURRENCY=auto' >> ~/.zprofile
# Launch the MCP server (read by an MCP-aware agent over stdio)
brew mcp-server
Output: (concurrent download bars, then standard install summary)
==> Downloading https://ghcr.io/v2/homebrew/core/jq/manifests/1.7.1 [1/3]
==> Downloading https://ghcr.io/v2/homebrew/core/ripgrep/manifests/14.1.1 [2/3]
==> Downloading https://ghcr.io/v2/homebrew/core/fd/manifests/10.1.0 [3/3]
==> Pouring jq--1.7.1.arm64_sonoma.bottle.tar.gz
==> Pouring ripgrep--14.1.1.arm64_sonoma.bottle.tar.gz
==> Pouring fd--10.1.0.arm64_sonoma.bottle.tar.gz
A handful of environment variables introduced or repurposed in 4.5/4.6 are worth knowing:
| Variable | Effect |
|---|---|
HOMEBREW_DOWNLOAD_CONCURRENCY | Parallel download count (auto or integer) |
HOMEBREW_FORBID_CASKS | Refuse to install any cask in this shell |
HOMEBREW_NO_ANALYTICS | Disable usage telemetry |
HOMEBREW_VERIFY_ATTESTATIONS | Verify GitHub Actions attestations for bottles (must be explicitly set in 4.6, even for developers) |
HOMEBREW_ASK | Make --ask the implicit default for brew install |
Apple Silicon vs Intel prefixes
Homebrew's installation prefix differs by CPU architecture: /opt/homebrew on Apple Silicon (arm64) and /usr/local on Intel (x86_64). The two are intentionally separate so a Mac that runs both architectures (e.g. via Rosetta 2) can keep an x86_64 Homebrew alongside the native arm64 one. The implication: every script that calls brew needs to either resolve the prefix dynamically ($(brew --prefix)) or call the architecture-specific binary directly.
# Resolve at runtime — portable across architectures
echo "$(brew --prefix)/bin"
# Add both prefixes to PATH in shell rc (Apple Silicon comes first)
eval "$(/opt/homebrew/bin/brew shellenv)"
# Run an x86_64 Homebrew under Rosetta from an arm64 shell
arch -x86_64 /usr/local/bin/brew install some-x86-only-formula
# Tell brew to use a specific Cellar (advanced)
HOMEBREW_CELLAR=/opt/homebrew/Cellar brew list
Output:
/opt/homebrew/bin
| Architecture | Default prefix | Cellar | Bin |
|---|---|---|---|
| Apple Silicon (arm64) | /opt/homebrew | /opt/homebrew/Cellar | /opt/homebrew/bin |
| Intel (x86_64) | /usr/local | /usr/local/Cellar | /usr/local/bin |
| Linux | /home/linuxbrew/.linuxbrew | /home/linuxbrew/.linuxbrew/Cellar | /home/linuxbrew/.linuxbrew/bin |
Dependencies and the leaf set
brew deps walks a formula's dependency graph; brew uses does the inverse — which formulae depend on the one you're inspecting; brew leaves lists formulae that nothing else depends on (the set you actually requested, modulo upgrade artefacts). Combine them to prune unused dependencies after a long stretch of installs and uninstalls.
# Full tree for a single formula
brew deps --tree ffmpeg
# Reverse — what depends on python@3.12?
brew uses --installed python@3.12
# Show only formulae that aren't a dependency of anything else
brew leaves
# Auto-remove unused dependencies (analogue of `apt autoremove`)
brew autoremove
# Dry-run first
brew autoremove --dry-run
Output (brew leaves):
bat
docker-compose
fd
ffmpeg
fzf
gh
git
jq
node@22
postgresql@16
python@3.12
ripgrep
tldr
Common pitfalls
brew: command not foundafter install — the installer's final caveat asks you to paste aeval "$(/opt/homebrew/bin/brew shellenv)"line into~/.zprofile. Skip it once and brew won't be on$PATHin new shells. Re-run that line.Error: The following directories are not writable by your user: /usr/local/share— usually a leftover from a previous Intel install that ran withsudo. Fix withsudo chown -R $(whoami) $(brew --prefix)/*./opt/homebrewpermissions reset after a macOS upgrade —chown -R $(whoami):admin /opt/homebrewre-grants ownership.Error: Cask 'foo' depends on hardware architecture being one of [...]— the cask is x86-only. Either install Rosetta 2 (softwareupdate --install-rosetta --agree-to-license) and runarch -x86_64 brew install --cask foo, or pick anarm64-native cask.brew servicesshows error but no log — service was started as root previously and the user-mode plist lacks permission to write to its log path. Runsudo brew services cleanupand re-start in the correct scope.- Two formulae provide the same binary (
unlink ...) —brew link --overwriteforces the most recently requested formula to win, but the nextbrew upgrademay re-link the other. Pin the one you want to keep linked. brew updateis slow — every tap is a git repo that has to begit pulled.brew untaptaps you don't need (brew tapto list,brew untapto remove), and setHOMEBREW_NO_AUTO_UPDATE=1to skip the implicitupdatebefore everyinstall.- Bottle download fails behind a corporate proxy — set
HOMEBREW_FORCE_BREWED_CURL=1and configureHTTPS_PROXY/ALL_PROXY; the systemcurlignores~/.curlrcproxy settings. - Casks installed as
--no-quarantinestill trigger Gatekeeper on first launch — re-quarantine was added by macOS as a separate check. Usexattr -dr com.apple.quarantine /Applications/AppName.appafter install. brew cleanupreclaims less than expected — only the previous version of each formula is removed by default. Usebrew cleanup --prune=all -sto also wipe the download cache.
Real-world recipes
Bootstrap a brand-new Mac from a checked-in Brewfile
The single most valuable Homebrew workflow: a Brewfile in your dotfiles repo lets you re-create a fully-loaded developer machine in a single command. Wire the install line into your dotfiles install.sh so the entire flow is one shell line away.
# 1. Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. Put brew on PATH for this shell
eval "$(/opt/homebrew/bin/brew shellenv)"
# 3. Run the Brewfile
cd ~/dotfiles && brew bundle install --file=Brewfile
# 4. Verify
brew bundle check --file=Brewfile
Output:
Homebrew Bundle complete! 42 Brewfile dependencies now installed.
The Brewfile's dependencies are satisfied.
Periodic full-machine upgrade and report
A weekly maintenance script that updates Homebrew, upgrades everything, prunes leftover downloads, and emails a summary to you using mailx.
#!/usr/bin/env bash
set -euo pipefail
log=/tmp/brew-weekly-$(date +%F).log
exec > >(tee "$log") 2>&1
echo "=== brew update ==="
brew update
echo "=== brew outdated (before) ==="
brew outdated
echo "=== brew upgrade ==="
brew upgrade
echo "=== brew upgrade --cask --greedy ==="
brew upgrade --cask --greedy
echo "=== brew cleanup ==="
brew cleanup --prune=all -s
echo "=== brew doctor ==="
brew doctor || true
mail -s "Mac weekly brew report" alice@example.com < "$log"
Output: (none — exits 0 on success)
Wire it into a launch agent that fires every Sunday at 09:00 — see the launchctl cheat sheet for the plist.
Disable telemetry permanently
Homebrew sends anonymous install analytics to InfluxData by default. Disable it once in your shell profile:
# Add to ~/.zshrc
export HOMEBREW_NO_ANALYTICS=1
# Verify the API call is no longer made
brew analytics state
Output:
Analytics are disabled.
InfluxDB analytics are disabled.
Build a private internal tap
When your company ships internal CLI tools, a private tap lets every team-mate install them with brew install company/internal/tool-name.
# 1. Create the repo (must be named homebrew-<tap-name>)
gh repo create mycompany/homebrew-internal --private
# 2. Clone and lay out the formula
git clone git@github.com:mycompany/homebrew-internal.git
cd homebrew-internal
mkdir -p Formula Casks
cat > Formula/internal-cli.rb <<'EOF'
class InternalCli < Formula
desc "MyCompany's internal admin CLI"
homepage "https://internal.example.com/internal-cli"
url "https://github.com/mycompany/internal-cli/releases/download/v1.0.0/internal-cli-1.0.0.tar.gz"
sha256 "deadbeef…"
license "MIT"
def install
bin.install "internal-cli"
end
test do
assert_match "internal-cli v1.0.0", shell_output("#{bin}/internal-cli --version")
end
end
EOF
git add Formula/internal-cli.rb
git commit -m "Add internal-cli formula"
git push
# 3. Team-mates tap and install
brew tap mycompany/internal git@github.com:mycompany/homebrew-internal.git
brew install mycompany/internal/internal-cli
Output: (none — exits 0 on success)
Switch Node.js versions without nvm
When node@20 and node@22 are both installed, swap which one's on $PATH in seconds.
# Currently linked
which node # /opt/homebrew/bin/node
node --version # v22.2.0
# Swap to node@20
brew unlink node
brew link --overwrite node@20
node --version # v20.13.1
# Back to the default
brew unlink node@20
brew link --overwrite node
Output: (none — exits 0 on success)
Remove every cask that wasn't installed via Brewfile
Useful before regenerating a Brewfile from scratch — strip the machine down to a known state.
# Show what would be removed
brew bundle cleanup --file=~/Brewfile --dry-run
# Apply
brew bundle cleanup --file=~/Brewfile --force
Output:
Would uninstall casks:
google-chrome
spotify
vlc
Find what installed a given file
When a stray binary appears in /opt/homebrew/bin, trace it back to its formula.
# Search the manifest of every installed keg
brew which-formula --installed ffprobe
# Or grep the receipt files directly
grep -l '"ffprobe"' /opt/homebrew/Cellar/*/*/INSTALL_RECEIPT.json
Output:
ffmpeg
/opt/homebrew/Cellar/ffmpeg/7.0.1/INSTALL_RECEIPT.json
Pin Postgres at 16 while everything else upgrades
You want fleet-wide upgrades but Postgres bumps the on-disk format between major versions — pin it.
brew install postgresql@16
brew pin postgresql@16
brew services start postgresql@16
# Confirm
brew list --pinned
Output:
postgresql@16
Subsequent brew upgrade runs will skip postgresql@16 until you brew unpin postgresql@16.
Reset a broken Homebrew without reinstalling everything
When permissions, taps, or symlinks get into a wedged state, this rebuilds the prefix without losing installed software.
# 1. Take ownership of the prefix
sudo chown -R "$(whoami):admin" "$(brew --prefix)"
# 2. Reset the core tap (most common cause of weird breakage)
brew untap homebrew/core && brew tap homebrew/core
# 3. Repair links
brew link --overwrite $(brew list --formula)
# 4. Re-check
brew doctor
brew missing
Output: (none — exits 0 on success)
Related cheat sheets
- macos-cli — broader macOS terminal reference
- defaults — preferences scripting after a brew bundle
- launchctl — manage
brew servicesplists directly - winget — Windows analogue
Sources
- Homebrew 4.6.0 release notes — concurrent downloads,
brew mcp-server, preliminary Tahoe support, default-branch rename tomain. - Homebrew 4.5.0 release notes —
brew bundle/brew servicesbecome built-in, Portable Ruby 3.4, new--ask/--skip-link/--as-dependencyinstall flags. - Homebrew 4.4.0 release notes — official macOS Sequoia (15) support and
INSTALL_RECEIPT.jsonfor casks. - Homebrew man page — canonical reference for every command, flag, and environment variable.
- Homebrew GitHub releases — full changelog for every 4.x point release.