How Homebrew Actually Works: Formulae, Casks, and the Cellar
What actually happens on disk when you brew install something, and why Homebrew's design differs from a traditional Linux package manager.
macOS ships without a package manager of its own, which is the gap Homebrew has filled since 2009 for most developers on the platform. Unlike APT or DNF, Homebrew isn’t built into the OS and doesn’t require root — a deliberate design choice that shapes almost everything else about how it works.
Formulae: Ruby scripts that describe a build
A Homebrew formula is a Ruby file describing how to obtain, build, and install one piece of software — conceptually similar to a FreeBSD port’s Makefile, but expressed as Ruby DSL instead of make syntax.
class Wget < Formula
desc "Internet file retriever"
homepage "https://www.gnu.org/software/wget/"
url "https://ftp.gnu.org/gnu/wget/wget-1.24.5.tar.gz"
sha256 "..."
depends_on "openssl@3"
def install
system "./configure", "--prefix=#{prefix}"
system "make", "install"
end
end
brew install wget
brew edit wget # open the formula itself
brew info wget
Bottles: pre-built binaries, most of the time
In practice, brew install almost never actually compiles anything — it downloads a bottle, a pre-built binary archive matching your exact macOS version and CPU architecture, built by Homebrew’s own CI infrastructure from the formula. The formula itself is really the build recipe; the bottle is the pre-baked result, and compiling locally only happens as a fallback when no matching bottle exists.
brew install --build-from-source wget
The Cellar: where everything actually lives
Every installed formula lands in the Cellar, isolated by name and version, under Homebrew’s prefix — /opt/homebrew on Apple Silicon, /usr/local on Intel Macs:
ls /opt/homebrew/Cellar/wget/
# 1.24.5
ls /opt/homebrew/Cellar/wget/1.24.5/bin/
Homebrew then symlinks each installed formula’s binaries, libraries, and headers from its isolated Cellar directory into shared top-level directories (/opt/homebrew/bin, /opt/homebrew/lib) that are actually on your PATH:
ls -la /opt/homebrew/bin/wget
# /opt/homebrew/bin/wget -> ../Cellar/wget/1.24.5/bin/wget
This symlink-forest design is precisely why multiple versions can coexist without conflict, and why brew unlink/brew link can switch which installed version is actually active without touching the Cellar contents at all:
brew unlink wget
brew link [email protected]
Casks: GUI applications, not command-line tools
A cask is Homebrew’s equivalent for GUI macOS applications distributed as .app bundles inside a .dmg or .zip, rather than compiled command-line software — installing a cask essentially automates what you’d otherwise do by hand: mount the disk image, drag the .app into /Applications, and clean up afterward.
brew install --cask visual-studio-code
brew list --cask
Dependency resolution and the graph
brew deps and brew uses let you inspect a formula’s dependency graph in either direction — what it needs, and what would break if you removed it:
brew deps --tree wget
brew uses --installed openssl@3
brew autoremove cleans up dependencies that were pulled in for something else but are no longer required by anything currently installed, and brew cleanup removes old versions’ Cellar directories once a newer bottle has replaced them.
Taps: extending beyond the core repository
The default set of formulae lives in Homebrew’s core tap, but brew tap can add additional formula repositories entirely — the mechanism third-party projects use to distribute their own Homebrew-installable packages without needing to be accepted into homebrew-core:
brew tap someorg/tools
brew install someorg/tools/customtool
No root, no system integration by design
The most consequential architectural choice in Homebrew is that it deliberately avoids requiring root privileges for day-to-day use and doesn’t touch anything under /System — everything lives under its own prefix, owned by your user account. That’s a direct response to macOS’s increasingly strict System Integrity Protection, and it’s why Homebrew has stayed viable across a decade of macOS versions that have made writing to system directories progressively harder: it never tried to in the first place.