Skip to content
macOSDeep Dive Published Updated 6 min readViews unavailable

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.

Where Homebrew actually came from

Max Howell created Homebrew as a simple Ruby script, with the first commit — a README describing the project’s design — appearing on GitHub on May 20, 2009, followed by the actual working script the next day, May 21. The motivating gap was concrete and specific: macOS shipped with Ruby pre-installed but no real package manager of its own, and the existing alternatives at the time (MacPorts, Fink) each had their own friction Howell wanted to design around directly — MacPorts and Fink both built from source into their own separate directory trees requiring sudo, whereas Homebrew’s original pitch was installing formulae into /usr/local using ordinary user permissions, leveraging directories Apple’s own tools mostly left alone. The project attracted rapid developer interest, quickly becoming one of GitHub’s most-starred repositories within its first year — a growth trajectory that outpaced Howell’s own original expectations for what had started as a personal itch-scratching script.

From a personal project to independent infrastructure

Homebrew’s early years ran largely on Howell’s own personal GitHub account and whatever infrastructure he could arrange, until a March 2013 Kickstarter campaign raised funds specifically for dedicated servers to build and test formulae at a scale a single maintainer’s own resources couldn’t sustain. Later that same year, on December 13, 2013, the project migrated from Howell’s personal account to its own dedicated Homebrew organization — a governance shift from “one person’s project that happens to be popular” to genuine shared infrastructure with its own maintainer team, mirroring a transition many successful single-maintainer open-source tools eventually go through as their user base outgrows what any one person can reasonably support alone.

brew doctor and brew bundle: keeping an installation healthy and reproducible

Beyond individual package installation, brew doctor audits the current Homebrew installation for common configuration problems — permission issues, conflicting files outside Homebrew’s own management, outdated symlinks — and reports specific, actionable warnings rather than a generic health score. brew bundle, working from a Brewfile listing every formula and cask a specific setup depends on, lets an entire development environment’s package list be captured as a single checked-in file and reproduced identically on another machine with brew bundle install — a genuinely useful pattern for onboarding a new machine or keeping a team’s development environment consistent, functioning as something like a package.json or requirements.txt equivalent for the system-level tools Homebrew itself manages, rather than the language-specific dependency files those tools normally cover only for a single project’s own direct code dependencies.

Why the split between /opt/homebrew and /usr/local exists at all

Apple Silicon’s default Homebrew prefix moved to /opt/homebrew specifically to avoid ambiguity with /usr/local, a path Intel Macs used for decades and one Rosetta-translated Intel binaries might still expect to find Intel-built libraries under. Keeping the two architectures’ installations in genuinely separate prefixes, rather than trying to make one shared /usr/local serve both natively-built Apple Silicon binaries and translated Intel ones simultaneously, avoids an entire category of subtle architecture-mismatch bugs that a shared prefix would otherwise risk introducing.

Related:

Sources:

Comments