The most surprising thing about managing multiple Homebrew environments is that you’re probably not using brew prefix effectively, and it’s not just for installing things in non-standard locations.
Let’s see it in action. Imagine you’re a developer working on two projects, project-a and project-b. Project A needs Python 3.9 and a specific version of redis, while Project B requires Python 3.11 and a newer redis that conflicts with Project A’s. Standard Homebrew installs would clobber each other.
Here’s how you’d set up isolated environments:
First, define your prefixes. Let’s say you want /usr/local/opt/brew_a for Project A and /usr/local/opt/brew_b for Project B.
mkdir -p /usr/local/opt/brew_a
mkdir -p /usr/local/opt/brew_b
Now, install packages into these specific prefixes. You’ll use the BREW_PREFIX environment variable.
For Project A:
BREW_PREFIX=/usr/local/opt/brew_a brew install python@3.9 redis@6.2
And for Project B:
BREW_PREFIX=/usr/local/opt/brew_b brew install python@3.11 redis
When you need to work on Project A, you activate its environment:
eval $(BREW_PREFIX=/usr/local/opt/brew_a brew shellenv)
Now, which python will point to the Python 3.9 installed in /usr/local/opt/brew_a, and redis-server --version will show 6.2.
When you switch to Project B:
eval $(BREW_PREFIX=/usr/local/opt/brew_b brew shellenv)
which python now points to 3.11, and redis-server --version shows the newer version.
This isn’t just about avoiding dependency hell. It’s about creating truly independent development sandboxes. Each BREW_PREFIX acts as a self-contained Homebrew installation, with its own bin, lib, include, and share directories. When brew shellenv is sourced with a BREW_PREFIX, it modifies your PATH and other environment variables to prioritize the binaries and libraries within that specific prefix. This means brew install and brew uninstall commands, when run within an activated BREW_PREFIX environment, operate solely on that isolated installation.
The core problem this solves is dependency conflict. Without prefix isolation, installing a package like redis would update the single, system-wide installation managed by your default Homebrew prefix (usually /usr/local). If another project needs an older, incompatible version, you’re stuck. Prefix isolation allows you to have multiple, distinct versions of the same package installed simultaneously, each accessible only when its corresponding environment is activated.
Consider the brew shellenv command. When you run eval $(BREW_PREFIX=/path/to/prefix brew shellenv), Homebrew outputs a series of export commands that modify your shell’s environment. Crucially, it prepends the bin directory of your specified BREW_PREFIX to your PATH. This ensures that when you type python or redis-server, your shell finds the version within the active BREW_PREFIX first. It also sets HOMEBREW_PREFIX and HOMEBREW_CELLAR to point to your chosen prefix, influencing other Homebrew operations.
This strategy is incredibly powerful for managing different language runtimes (Python 2 vs. 3, multiple Node.js versions), databases, or any software with complex or conflicting dependencies. You can create a brew_py39, brew_py311, brew_node16, brew_node18 etc., and switch between them seamlessly.
The real magic of brew prefix and brew shellenv is that they don’t actually install separate Homebrew installations. Instead, they leverage Homebrew’s ability to manage "kegs" (individual package versions) within a single CELLAR, but then use BREW_PREFIX to create isolated views of those installed packages by manipulating your shell’s environment. You can have python@3.9 and python@3.11 installed in your main Homebrew CELLAR (e.g., /usr/local/Cellar/python@3.9/3.9.18 and /usr/local/Cellar/python@3.11/3.11.5), but eval $(BREW_PREFIX=/usr/local/opt/brew_a brew shellenv) will make /usr/local/opt/brew_a/bin (which contains symlinks to the correct keg) the primary source for your python executable.
When you install a package into a BREW_PREFIX, Homebrew creates a symlink structure within that prefix’s bin, lib, etc., pointing to the actual installed version in the CELLAR. So, a BREW_PREFIX installation is essentially a custom set of symlinks managed by Homebrew itself. This means you don’t need to download and manage multiple full Homebrew installations, saving disk space and simplifying updates. The brew cleanup command, when run within an activated BREW_PREFIX, will also respect that prefix, only cleaning up packages associated with it.
The next hurdle you’ll likely encounter is managing these eval commands across different terminal sessions and projects, which often leads to exploring shell configuration files like .bashrc or .zshrc and potentially using tools like direnv.