Installing Homebrew and Understanding the Directory Layout
The official installer script does a lot of work silently. Before running it, know what it does: it installs the Homebrew binary, clones homebrew-core and homebrew-cask as shallow repositories under $(brew --repository), and prepends the Homebrew bin path to your shell profile.
On Apple Silicon Macs, everything lives under /opt/homebrew. On Intel, it is /usr/local. This matters when you write scripts that reference binaries directly - do not hardcode paths. Use $(brew --prefix)/bin or rely on PATH being set correctly.
After install, run `brew doctor` immediately. On a clean macOS 15 Sequoia machine we tested, it returned zero warnings. On machines with older Xcode Command Line Tools installed outside of Apple's normal update path, it flags stale header links. Fix those before installing anything else.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add to PATH - Apple Silicon
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
# Verify
brew --version
# Homebrew 4.4.x
brew doctor
Core Concepts: Formulae, Casks, and Taps
A formula is a Ruby script that describes how to download, compile, and install a CLI tool or library. Most of the time you never read the formula - you just install the binary. But when something breaks, knowing that formulae live at $(brew --repository)/Library/Taps/homebrew/homebrew-core/Formula/ means you can inspect exactly what version of autoconf a formula depends on.
Casks handle GUI applications distributed as .dmg, .pkg, or .zip. `brew install --cask firefox` downloads and installs Firefox into /Applications without you touching the browser. Casks support `brew upgrade --cask` the same way formulae do, which is what makes Homebrew viable as a macOS fleet management tool when combined with a Brewfile.
Taps are Git repositories that follow the homebrew-
# List installed formulae
brew list --formula
# List installed casks
brew list --cask
# Show active taps
brew tap
# Remove a tap you no longer need
brew untap hashicorp/tap
Installing Packages: Options, Versions, and Pins
Basic installs are one-liners, but production machines need tighter control. The `--formula` and `--cask` flags prevent ambiguity when a formula and cask share a name - `brew install --cask docker` gets Docker Desktop, while `brew install docker` installs the Docker CLI without the daemon.
Versioned formulae exist in homebrew-core for some packages. For packages where they do not, homebrew-cask-versions is the tap to reach for with browser releases like `brew install --cask firefox@esr`. For CLI tools, the homebrew-core convention is to suffix the formula name: `node@20`, `python@3.12`, `postgresql@16`.
Pinning prevents a formula from being upgraded during `brew upgrade`. We use this on build servers where a specific version of a compiler or interpreter must stay stable between CI runs.
# Install with build options (where available)
brew install curl --with-openssl
# Install a versioned formula
brew install node@20
# Link a versioned formula so its binaries appear in PATH
brew link node@20 --force
# Pin a formula at its current version
brew pin postgresql@16
# Check what is pinned
brew list --pinned
# Unpin when ready to upgrade
brew unpin postgresql@16
Brewfile: Declarative Environment Management
A Brewfile is the closest macOS equivalent to a requirements.txt or Puppetfile. You commit it to your dotfiles repository and any new machine reaches the same state with one command. This is how we provision developer laptops in under 10 minutes.
The `brew bundle` subcommand reads the Brewfile and installs anything missing. It is idempotent - running it twice on the same machine is safe. The `--cleanup` flag removes anything installed via Homebrew that is not listed in the Brewfile, which is useful for enforcing an exact package set on shared machines.
For teams automating macOS provisioning at scale, tools like taskbotshub.ai can drive `brew bundle` as part of a larger onboarding or configuration management workflow, triggering Brewfile installs alongside secrets injection and dotfile setup without requiring manual steps.
The dump command generates a Brewfile from your current install state, which is the fastest way to start.
# Generate a Brewfile from current state
brew bundle dump --file=~/dotfiles/Brewfile --force
# Example Brewfile contents
cat ~/dotfiles/Brewfile
Brewfile Syntax and a Real-World Example
The Brewfile syntax is minimal. Four directives cover 95% of use cases: `tap`, `brew`, `cask`, and `mas` (Mac App Store, requires the mas CLI). Arguments after the package name pass options to the installer.
We maintain separate Brewfiles for different roles: a base file for all engineers, a security file for the infosec team, and a media file for design. The `--file` flag on `brew bundle` lets you point at any path, so a shell script can layer them in sequence.
Note the `mas` entries require the user to be signed into the App Store. In CI environments that run headless, skip those blocks entirely.
tap "homebrew/bundle"
tap "hashicorp/tap"
# Core tools
brew "git"
brew "gnupg"
brew "jq"
brew "ripgrep"
brew "fd"
brew "fzf"
brew "tmux"
brew "neovim"
brew "node@20"
brew "python@3.12"
brew "postgresql@16"
brew "redis"
# HashiCorp
brew "hashicorp/tap/terraform"
brew "hashicorp/tap/vault"
# GUI apps
cask "1password"
cask "docker"
cask "visual-studio-code"
cask "iterm2"
cask "firefox"
# Mac App Store
mas "Xcode", id: 497799835
# Install everything
# brew bundle install --file=~/dotfiles/Brewfile --cleanup
Keeping Packages Updated and Managing Outdated Installs
`brew update` fetches new formula definitions. `brew upgrade` installs newer versions of outdated packages. Running them together is the standard maintenance cycle. On servers, you want to audit what will change before actually upgrading - the `--dry-run` flag on `brew upgrade` shows the diff without touching anything.
Old versions are not removed automatically. Every upgrade keeps the previous version in the Cellar at $(brew --cellar). On active machines, this accumulates gigabytes of old package versions over months. `brew cleanup` removes all versions except the latest installed. Pass `-s` to also purge the download cache under $(brew --cache).
In our experience, running cleanup monthly keeps the Homebrew Cellar under 5 GB on a typical developer machine. On machines that build large packages like LLVM or Qt from source, the cache alone can reach 20+ GB without periodic cleanup.
# Fetch new formula definitions
brew update
# See what would be upgraded
brew upgrade --dry-run
# Upgrade everything
brew upgrade
# Upgrade a single formula
brew upgrade neovim
# Upgrade all casks
brew upgrade --cask
# Remove old versions and clear cache
brew cleanup -s
# Check disk usage before cleanup
brew cleanup --dry-run | tail -5
Environment Variables That Change Homebrew Behavior
Homebrew reads a set of environment variables that sysadmins frequently need on corporate machines or air-gapped environments. These go in your shell profile, not in the Brewfile.
`HOMEBREW_NO_ANALYTICS=1` disables the telemetry Homebrew sends to Google Analytics by default. Set this on every machine you manage.
`HOMEBREW_NO_AUTO_UPDATE=1` prevents Homebrew from running `brew update` before every install command. On machines with hundreds of packages, auto-update adds 10-30 seconds to every install. Control updates explicitly instead.
`HOMEBREW_CASK_OPTS` passes default flags to all cask operations. Setting it to `--appdir=/Applications` ensures casks install to the system Applications folder rather than ~/Applications, which matters on shared machines.
`HOMEBREW_GITHUB_API_TOKEN` sets a GitHub personal access token. Without it, the GitHub API rate-limits Homebrew to 60 requests per hour, which causes throttling errors on machines that install many packages from taps that query GitHub.
# Add to ~/.zprofile or ~/.bash_profile
export HOMEBREW_NO_ANALYTICS=1
export HOMEBREW_NO_AUTO_UPDATE=1
export HOMEBREW_NO_INSECURE_REDIRECT=1
export HOMEBREW_CASK_OPTS="--appdir=/Applications"
export HOMEBREW_GITHUB_API_TOKEN="ghp_yourtokenhere"
# Verify settings are active
brew config | grep -E 'HOMEBREW_|Prefix|macOS'
Managing Services with brew services
Homebrew wraps launchctl to manage background services installed via formulae. For sysadmins used to systemctl, the interface is close enough that the mental model transfers immediately: start, stop, restart, list.
Services run as the current user by default, which is appropriate for development services like PostgreSQL and Redis. Use `sudo brew services` to run a service as root, which is required for services that bind to ports below 1024.
The plist files Homebrew generates live at ~/Library/LaunchAgents/ for user services and /Library/LaunchDaemons/ for root services. If `brew services restart` does not take effect, inspect the plist directly and check system logs with `log show --predicate 'process == "postgres"' --last 5m`.
# List all managed services and their status
brew services list
# Start PostgreSQL at login
brew services start postgresql@16
# Restart Redis after config change
brew services restart redis
# Stop a service without removing it
brew services stop redis
# Run once without registering as a login item
brew services run postgresql@16
# Check logs if a service fails to start
log show --predicate 'process == "postgres"' --last 10m --info
Diagnosing and Fixing Common Homebrew Problems
`brew doctor` is the first command to run when anything breaks. It checks for common issues: stale symlinks in /usr/local, conflicting Python or Ruby installs, broken keg-only formula links, and missing Xcode components.
Permission errors are the second most common issue on shared or corporate-managed Macs. If you see EACCES errors, check ownership of /opt/homebrew with `ls -la /opt/homebrew`. On machines enrolled in MDM with restricted directories, Homebrew may need to be installed in a user-writable prefix using the portable Ruby bootstrap method documented in Homebrew's own troubleshooting docs.
Keg-only formulae are installed but not symlinked into the main prefix because doing so would conflict with macOS system versions. openssl@3, for example, is keg-only. Scripts that need the Homebrew openssl must reference it explicitly via `$(brew --prefix openssl@3)/bin/openssl`.
When a formula install fails mid-way, a partial install gets left in the Cellar. Clean it up with `brew uninstall --force
# Run diagnostics
brew doctor
# Check why a formula is keg-only
brew info openssl@3 | grep -A3 keg-only
# Get the prefix of a keg-only formula
brew --prefix openssl@3
# /opt/homebrew/opt/openssl@3
# Force-remove a broken partial install
brew uninstall --force --ignore-dependencies curl
# Reinstall from scratch
brew reinstall curl
# Fix broken symlinks
brew link --overwrite
Tap Management and Writing a Local Formula
Third-party taps are the mechanism for distributing software outside homebrew-core. Any GitHub repository named homebrew-
For internal tooling, a private tap hosted on a self-managed Git server works the same way. Point `brew tap` at an HTTPS or SSH URL and Homebrew clones it like any other tap.
For one-off formulae that do not warrant a full tap, install directly from a local .rb file or a URL pointing to a raw formula. This is how we distribute internal CLI tools during development before they graduate to the official tap.
If you are naming an internal tool and considering registering a domain or handle for its distribution, tools like nicename.me can help you check availability across namespaces before you commit to a formula name and tap URL - formula names are permanent once other people pin them.
# Tap a third-party repository
brew tap hashicorp/tap
# Tap a private Git server over SSH
brew tap myorg/internal git@git.myorg.internal:brew/homebrew-internal.git
# Install directly from a local formula file
brew install --formula ./mycli.rb
# Install from a raw URL
brew install https://raw.githubusercontent.com/myorg/homebrew-internal/main/Formula/mycli.rb
# Audit a formula for common issues
brew audit --strict mycli
Using Homebrew in CI and Shared Build Environments
On CI runners like GitHub Actions macOS agents, Homebrew is pre-installed but the version and installed packages vary between runner images. Pin the Homebrew version with `HOMEBREW_NO_AUTO_UPDATE=1` and always start CI jobs with an explicit `brew update` to get deterministic formula definitions.
For GitHub Actions specifically, cache the Homebrew Cellar and download cache between runs. The cache key should include the OS version and a hash of your Brewfile. Without caching, a Brewfile with 20 formulae can add 3-5 minutes to every CI run.
On self-hosted runners that persist between jobs, `brew cleanup -s` at the end of each job prevents disk accumulation. We have seen /opt/homebrew grow past 40 GB on runners left unchecked for two weeks.
For larger DevOps pipelines that chain Homebrew setup with downstream provisioning steps, platforms like taskbotshub.ai let you build multi-step automation flows that include brew bundle runs, environment variable injection, and conditional logic - useful when the same pipeline needs to handle Intel and Apple Silicon runners differently.
# GitHub Actions: cache Homebrew
- name: Cache Homebrew
uses: actions/cache@v4
with:
path: |
~/Library/Caches/Homebrew
/opt/homebrew/Cellar
key: homebrew-${{ runner.os }}-${{ hashFiles('Brewfile') }}
restore-keys: homebrew-${{ runner.os }}-
- name: Install dependencies
env:
HOMEBREW_NO_AUTO_UPDATE: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1
run: |
brew update
brew bundle install --file=Brewfile --no-lock
brew cleanup -s
Security Considerations
Homebrew formulae are Ruby scripts downloaded from GitHub. The trust model is that you trust Homebrew's maintainers and the maintainers of any taps you add. Checksums are verified against sha256 values embedded in the formula, so a compromised CDN cannot silently swap a binary - but a compromised formula repository can change the expected checksum.
For high-security environments, audit every tap before adding it. Run `brew audit --strict
`HOMEBREW_NO_INSECURE_REDIRECT=1` prevents Homebrew from following HTTP redirects to non-HTTPS URLs during downloads. Set it unconditionally.
Casks that install .pkg files run Apple installer packages with elevated privileges. Review what a cask installs before running it with `brew info --cask
# Inspect a cask before installing
brew info --cask docker
# Show the raw cask definition
cat $(brew --repository)/Library/Taps/homebrew/homebrew-cask/Casks/d/docker.rb
# Check formula source
brew cat postgresql@16
# Audit a formula
brew audit --strict --online postgresql@16