Homebrew’s core magic lies in how it organizes installed software, and understanding its directory structure is key to debugging and advanced usage.

Let’s look at a typical Homebrew installation on macOS.

First, the prefix:

brew --prefix

This command will output something like /usr/local. This is the base directory where Homebrew installs everything. Think of it as the root of Homebrew’s universe. All subsequent directories we discuss will be located within this prefix.

Inside the prefix, you’ll find the cellar:

ls $(brew --prefix)/Cellar

This directory is where Homebrew keeps the actual, versioned installations of every package you install. You’ll see directories named after the formulae (e.g., node, git, wget), and inside those, subdirectories for each installed version (e.g., v18.12.0, 2.39.2, 1.21.4).

For example, if you installed node version 18.12.0, its files would reside in $(brew --prefix)/Cellar/node/18.12.0/. This isolation is crucial: it allows you to have multiple versions of the same software installed simultaneously and switch between them.

Homebrew uses symlinks to present a single, active version of each package to your system. These symlinks are located directly within the prefix, not in the Cellar.

ls $(brew --prefix)

You’ll see directories like bin, lib, include, share, etc. For instance, the node executable you run from your terminal isn’t directly in $(brew --prefix)/bin. Instead, it’s a symlink pointing to the node executable within the currently active version in the Cellar.

ls -l $(brew --prefix)/bin/node

This symlink points to a path like ../../Cellar/node/18.12.0/bin/node. This is how Homebrew makes a specific version appear as if it’s installed directly in your system’s PATH.

The Formula Directory Structure is a bit different. This isn’t where installed software lives, but where the instructions for installing software reside.

ls $(brew --prefix)/Homebrew/Library/Formula

These are Ruby files (e.g., node.rb, git.rb) that Homebrew reads to understand how to download, compile, and install each package. They contain definitions of dependencies, build flags, and where to find source code.

When you run brew install node, Homebrew finds node.rb in this directory, parses it, and then uses the instructions within to download, build, and install node into the Cellar.

Here’s a look at a simplified node.rb formula:

class Node < Formula
  desc "Platform independent, JavaScript runtime"
  homepage "https://nodejs.org/"
  url "https://nodejs.org/dist/v18.12.0/node-v18.12.0.tar.gz"
  sha256 "f83e53a389f5616090c95c3c831357d7744407954c19d53e39b3f093c625b532"

  depends_on "python" => :build

  def install
    # ... build and install steps ...
    bin.install "out/Release/node"
    # ... other installations ...
  end
end

This Ruby file tells Homebrew the URL of the source code, its checksum, dependencies, and crucially, the install method which specifies where in the Cellar and what files to place.

The $(brew --prefix)/Homebrew/Library/Taps directory is where Homebrew stores information about tapped repositories. Taps are essentially external sources of formulae. The default core Homebrew tap is located here.

ls $(brew --prefix)/Homebrew/Library/Taps

When you tap a new repository (e.g., brew tap ethereum/ethereum), Homebrew clones that repository into this Taps directory, making its formulae available for installation.

The most surprising aspect of Homebrew’s structure is how it handles multiple versions of the same package. It’s not just about having separate directories in the Cellar; it’s about how Homebrew manages the symlinks to present a single, cohesive environment. When you run brew switch node 18.12.0, you’re not moving files; you’re simply updating the symlinks in the prefix (like $(brew --prefix)/bin/node) to point to the new version’s files in the Cellar. This indirection is what makes version switching seamless and efficient.

The next concept you’ll encounter is how Homebrew manages these symlinks and the potential for conflicts or broken links when operations go awry.

Want structured learning?

Take the full Homebrew course →