Homebrew is less a package manager and more a tiny, opinionated Linux distribution for macOS.

Let’s see it in action. Imagine you want the latest version of git. Normally, macOS ships with an older version, and updating it system-wide is a pain. With Homebrew, it’s simple:

brew install git

That single command downloads git’s source code (or a pre-compiled binary), compiles it (if necessary), and installs it into its own isolated directory, /usr/local/Cellar/git/2.41.0 (the version number will vary). It then creates symbolic links from /usr/local/bin to the actual executables in that cellar directory. So, when you type git in your terminal, your shell finds the Homebrew-installed version first because /usr/local/bin is early in your $PATH.

The problem Homebrew solves is dependency management and versioning. Most software on macOS relies on other libraries (dependencies). If you install one app that needs libfoo version 1.0 and another that needs libfoo version 2.0, you have a conflict. Homebrew avoids this by installing each package and its dependencies into its own unique subdirectory within /usr/local/Cellar. This isolation means different versions of the same library can coexist peacefully.

Here’s how it works internally: Homebrew uses "formulae" – Ruby scripts that describe how to download, compile, and install a piece of software. When you run brew install <formula>, Homebrew reads the formula, downloads the source, runs the build commands specified, and places the resulting files into the Cellar. It then updates the symbolic links in /usr/local/bin, /usr/local/lib, etc., to point to the newly installed version.

The core commands you’ll use are:

  • brew install <formula>: Installs a package.
  • brew uninstall <formula>: Removes a package.
  • brew upgrade <formula>: Updates a package to its latest version.
  • brew search <keyword>: Finds packages.
  • brew list: Shows installed packages.
  • brew outdated: Lists packages that have newer versions available.

The "levers" you control are primarily the formulae themselves. You can:

  • Pin a version: If you need a specific version of a package and don’t want it to be upgraded automatically, you can "pin" it: brew pin <formula>. To unpin, use brew unpin <formula>.
  • Tap another repository: Homebrew has a core set of formulae, but many more are available in community-maintained "taps" (short for repositories). You can add a new tap with brew tap <user>/<repo>. For example, brew tap homebrew/cask-versions gives you access to older versions of macOS applications.
  • Edit a formula: For advanced users, you can even edit the Ruby formula for a package directly using brew edit <formula>. This is useful for patching or customizing the build process.

Most people think of Homebrew as just a way to get command-line tools, but its brew install --cask <app> command handles graphical applications too, managing their installation, updates, and uninstallation much like it does for command-line tools, placing them in /Applications. This makes managing everything from wget to Visual Studio Code remarkably consistent.

When you run brew update, it’s not just checking for new versions of your installed packages; it’s also updating the formulae themselves. This means Homebrew itself is constantly evolving, fetching the latest descriptions of how to build and install software.

The system can sometimes get confused about what’s installed where, especially if you’ve manually fiddled with files in /usr/local. Running brew doctor is your first line of defense, and it will usually tell you exactly what’s wrong and provide the command to fix it, like brew prune to clean up broken symlinks or brew link --overwrite <formula> to force a re-linking.

The next thing you’ll likely want to explore is Homebrew Cask, which extends Homebrew’s capabilities to GUI applications.

Want structured learning?

Take the full Homebrew course →