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.

#macos#package-manager#cliupdated 05-26-2026

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.

bash
# 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:

text
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:

bash
/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).

bash
brew <command> [<subcommand>] [<flags>] [<formula-or-cask>...]

Output: (none — exits 0 on success)

Essential options

FlagMeaning
--caskOperate on a cask (GUI app) rather than a formula
--formulaForce formula-mode when a name exists in both namespaces
--HEADInstall the upstream HEAD/main branch instead of the latest stable
--build-from-source, -sCompile locally even if a bottle is available
--force-bottleThe opposite — refuse to fall back to source
--quiet, -qSuppress informational output
--verbose, -vShow downloads, build steps, and post-install scripts
--dry-run, -nPrint what would happen without changing anything (most subcommands)
--debug, -dDrop into an interactive debugger on failure
--forceOverride safety checks (use sparingly)
--ignore-dependenciesSkip dependency resolution (useful for surgical fixes)
--only-dependenciesInstall 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.

bash
# 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:

text
==> 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:

bash
brew list
brew list --formula
brew list --versions
brew list --cask

Output:

text
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.

bash
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:

text
==> 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.

bash
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.

bash
# 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:

text
==> Formulae
node       node@18    node@20    node@22    node-build    nodebrew    nodenv

==> Casks
node-installer
text
==> 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.

bash
# 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:

text
==> 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:

bash
# 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:

text
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.

bash
# 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:

text
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.

bash
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:

ruby
# ~/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:

bash
# 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:

text
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.

bash
# 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:

text
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.

bash
# 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:

text
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.

bash
brew doctor
brew config
brew --env
brew missing       # formulae installed without their deps satisfied

Output (brew doctor — clean machine):

text
Your system is ready to brew.

Output (brew doctor — typical warnings):

text
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.
text
==> 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.

bash
# 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)

text
==> 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:

VariableEffect
HOMEBREW_DOWNLOAD_CONCURRENCYParallel download count (auto or integer)
HOMEBREW_FORBID_CASKSRefuse to install any cask in this shell
HOMEBREW_NO_ANALYTICSDisable usage telemetry
HOMEBREW_VERIFY_ATTESTATIONSVerify GitHub Actions attestations for bottles (must be explicitly set in 4.6, even for developers)
HOMEBREW_ASKMake --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.

bash
# 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:

text
/opt/homebrew/bin
ArchitectureDefault prefixCellarBin
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.

bash
# 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):

text
bat
docker-compose
fd
ffmpeg
fzf
gh
git
jq
node@22
postgresql@16
python@3.12
ripgrep
tldr

Common pitfalls

  1. brew: command not found after install — the installer's final caveat asks you to paste a eval "$(/opt/homebrew/bin/brew shellenv)" line into ~/.zprofile. Skip it once and brew won't be on $PATH in new shells. Re-run that line.
  2. Error: The following directories are not writable by your user: /usr/local/share — usually a leftover from a previous Intel install that ran with sudo. Fix with sudo chown -R $(whoami) $(brew --prefix)/*.
  3. /opt/homebrew permissions reset after a macOS upgradechown -R $(whoami):admin /opt/homebrew re-grants ownership.
  4. 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 run arch -x86_64 brew install --cask foo, or pick an arm64-native cask.
  5. brew services shows error but no log — service was started as root previously and the user-mode plist lacks permission to write to its log path. Run sudo brew services cleanup and re-start in the correct scope.
  6. Two formulae provide the same binary (unlink ...)brew link --overwrite forces the most recently requested formula to win, but the next brew upgrade may re-link the other. Pin the one you want to keep linked.
  7. brew update is slow — every tap is a git repo that has to be git pulled. brew untap taps you don't need (brew tap to list, brew untap to remove), and set HOMEBREW_NO_AUTO_UPDATE=1 to skip the implicit update before every install.
  8. Bottle download fails behind a corporate proxy — set HOMEBREW_FORCE_BREWED_CURL=1 and configure HTTPS_PROXY/ALL_PROXY; the system curl ignores ~/.curlrc proxy settings.
  9. Casks installed as --no-quarantine still trigger Gatekeeper on first launch — re-quarantine was added by macOS as a separate check. Use xattr -dr com.apple.quarantine /Applications/AppName.app after install.
  10. brew cleanup reclaims less than expected — only the previous version of each formula is removed by default. Use brew cleanup --prune=all -s to 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.

bash
# 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:

text
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.

bash
#!/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:

bash
# Add to ~/.zshrc
export HOMEBREW_NO_ANALYTICS=1

# Verify the API call is no longer made
brew analytics state

Output:

text
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.

bash
# 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.

bash
# 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.

bash
# Show what would be removed
brew bundle cleanup --file=~/Brewfile --dry-run

# Apply
brew bundle cleanup --file=~/Brewfile --force

Output:

text
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.

bash
# 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:

text
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.

bash
brew install postgresql@16
brew pin postgresql@16
brew services start postgresql@16

# Confirm
brew list --pinned

Output:

text
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.

bash
# 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)

  • macos-cli — broader macOS terminal reference
  • defaults — preferences scripting after a brew bundle
  • launchctl — manage brew services plists directly
  • winget — Windows analogue

Sources

  • Homebrew 4.6.0 release notes — concurrent downloads, brew mcp-server, preliminary Tahoe support, default-branch rename to main.
  • Homebrew 4.5.0 release notesbrew bundle/brew services become built-in, Portable Ruby 3.4, new --ask / --skip-link / --as-dependency install flags.
  • Homebrew 4.4.0 release notes — official macOS Sequoia (15) support and INSTALL_RECEIPT.json for casks.
  • Homebrew man page — canonical reference for every command, flag, and environment variable.
  • Homebrew GitHub releases — full changelog for every 4.x point release.