Pinning a Homebrew package to a specific version is the only reliable way to prevent unexpected upgrades that can break your development environment.
Let’s see this in action. Imagine you have a project that absolutely needs Python 3.9. You installed it with brew install python@3.9. You’re happy. Then, a month later, you run brew upgrade because you’re a good citizen, and suddenly your project breaks. brew upgrade installed Python 3.11, and your project’s dependencies aren’t compatible. You just got bitten by the "latest and greatest" trap.
Here’s how to prevent that.
First, you need to know the exact version you want to pin. You can find available versions of a formula by looking at its history on GitHub. For example, to find past versions of python, you’d navigate to https://github.com/Homebrew/homebrew-core/commits/master/Formula/p/python.rb. Pick the commit corresponding to the version you need. Let’s say you want 3.9.18.
The core mechanism for pinning is brew pin. Once you’ve installed the specific version you want (e.g., brew install python@3.9), you then pin it:
brew pin python@3.9
This command creates a symbolic link from the main Cellar directory to the specific version’s directory, and importantly, it adds a .pin file to that version’s directory. This .pin file is what brew upgrade checks. When brew upgrade encounters a pinned formula, it skips it entirely.
What if you need to unpin? Simple:
brew unpin python@3.9
This removes the .pin file and allows brew upgrade to consider it again.
Now, what if you accidentally upgraded and need to downgrade? Homebrew keeps older versions in its cache. You can install a specific older version directly:
brew install python@3.9.18
If that version isn’t in your cache, Homebrew will download it. Once installed, you can then pin it as described above.
To see which packages are currently pinned, you can use:
brew list --pinned
This will show you a list of all formulas that have been pinned, along with their exact versions.
It’s crucial to understand that brew pin doesn’t stop you from installing newer versions of the same formula. If you run brew install python@3.11, it will be installed alongside your pinned python@3.9. The pinning only prevents brew upgrade from touching the pinned version. You can still manually switch between installed versions using brew switch (though this is less common now with brew link and brew unlink).
The real power comes when managing complex environments. For instance, if your project relies on a specific version of node and a specific version of imagemagick, you’d pin both after installing them:
brew install node@18
brew install imagemagick@6
brew pin node@18
brew pin imagemagick@6
This ensures that brew upgrade won’t touch these critical components.
The .pin file is just an empty file named pin in the formula’s installation directory within the Cellar. Homebrew’s upgrade logic specifically looks for this file. If it finds it, it skips the formula. This is why brew upgrade --force-bottle or brew upgrade --overwrite might seem to work around it, but they are generally discouraged as they can lead to unintended consequences. The .pin file is the explicit, intended way to manage version stability.
If you’ve ever encountered the error Error: Some installed formulae were not updated, it’s often because they were pinned. The fix is to either unpin them (brew unpin <formula>) if you want them upgraded, or to ignore the message if you intentionally pinned them.
The next hurdle you’ll likely face is managing dependencies of pinned packages. Sometimes, a newer version of a dependency might be required by a different project, and Homebrew’s dependency resolution might get confused if it can’t upgrade the pinned package’s dependencies.