Pinning a Homebrew package to a specific version lets you lock it down, preventing automatic upgrades that might break your workflow. This is a lifesaver when you rely on a particular feature set or a stable API that a newer version might have altered or removed.

Let’s see this in action. Imagine you’re using ffmpeg and need version 4.4.2 because a specific codec conversion only works reliably with that exact build.

# Check current ffmpeg version
brew list --versions ffmpeg
# Output might be: ffmpeg 6.0

# We want to pin to 4.4.2
brew unlink ffmpeg
brew install ffmpeg@4.4.2
brew link ffmpeg@4.4.2

# Now check again
brew list --versions ffmpeg
# Output should now show: ffmpeg 4.4.2

The core idea is that Homebrew manages formulae (packages) in distinct directories within its Cellar. When you install a package, it creates a symlink from /usr/local/opt/<package-name> (or equivalent for Apple Silicon) to the specific version in the Cellar. Pinning means telling Homebrew to not touch that symlink during brew upgrade.

Here’s how it works under the hood:

  1. Installation: When you brew install <package>, Homebrew downloads the source, compiles it, and places the resulting binaries and libraries into a versioned subdirectory within its Cellar (e.g., /usr/local/Cellar/ffmpeg/6.0/). It then creates a symlink, typically /usr/local/opt/ffmpeg, pointing to this directory. Your PATH (and other environment variables) is configured to find executables via this opt symlink.
  2. Upgrading: brew upgrade looks for installed packages that have newer versions available. If ffmpeg is installed and version 6.0 is current, and 6.1 is released, brew upgrade ffmpeg will:
    • Install ffmpeg 6.1 into /usr/local/Cellar/ffmpeg/6.1/.
    • Update the /usr/local/opt/ffmpeg symlink to point to /usr/local/Cellar/ffmpeg/6.1/.
    • If you had a specific older version installed, it might also remove it if it’s no longer the "latest" or if you run brew cleanup.
  3. Pinning: brew pin <package> essentially tells Homebrew, "Whatever you do, don’t change the symlink for <package>." It does this by creating a .pin file in the package’s Cellar directory. When brew upgrade encounters a .pin file, it skips that package.

To pin a package, you use the brew pin command:

brew pin <package-name>

For example, to pin openssl:

brew pin openssl

This will prevent brew upgrade openssl from updating it automatically. To unpin it later, you’d use brew unpin openssl.

It’s important to note that Homebrew often manages older versions of formulae as separate formulae, especially for major version changes (e.g., python@3.9, ffmpeg@4.4). When you want to pin to a specific minor or patch version that isn’t available as a separate formula, you might need to resort to manually installing it from a specific Git commit in the Homebrew core repository or a tap. However, for most common pinning needs, Homebrew’s versioned formulae and the pin command suffice.

A common pitfall is forgetting to pin after installing the desired version. If you brew install ffmpeg and it installs 6.0, and then you want 4.4.2, you must install ffmpeg@4.4.2 and then pin it. If you just brew pin ffmpeg while 6.0 is installed, you’re pinning 6.0, not the older version you might have intended. You’d typically want to uninstall the newer version, install the older one, and then pin the older one.

# Example workflow for pinning an older version
brew uninstall ffmpeg # Remove the current (e.g., 6.0)
brew install ffmpeg@4.4.2 # Install the specific older version
brew pin ffmpeg # Pin it to prevent upgrades

This process ensures that your critical dependencies remain stable, allowing you to iterate on your own code without unexpected environmental shifts.

The next hurdle you’ll likely face is managing multiple versions of the same package simultaneously for different projects.

Want structured learning?

Take the full Homebrew course →