Multi-Prefix Environments: Per-Project Package Isolation
Use stout's multi-prefix feature to install different package versions for different projects — no conflicts, no global pollution.
Every developer who works on more than one project has felt the version conflict pain. Project A needs PostgreSQL 15, project B needs PostgreSQL 16. Project C depends on Python 3.11 with specific library paths, while project D requires Python 3.12. With Homebrew, all packages install into a single global prefix (/opt/homebrew or /usr/local), and you can only have one active version of each package at a time. stout’s multi-prefix feature solves this by letting you create isolated package environments — each with its own Cellar, its own binaries, and its own versions — scoped to a project or any logical boundary you choose.
The global prefix problem
Homebrew’s single-prefix model made sense when it was primarily used to install Unix utilities on macOS. You needed git, wget, and tree available globally, and there was no reason for per-project isolation. But modern development workloads have changed. Developers routinely work on multiple services, each with different runtime and library version requirements.
The workarounds are familiar and unsatisfying:
brew linkandbrew unlink— manually switch between versions, forgetting which one is active- Version-suffixed formulae (
[email protected],[email protected]) — available for popular packages but not all, and PATH management is manual - Docker — full containerization works but adds overhead for simple “I need a different version of libpq” scenarios
- asdf/mise — great for runtimes but does not cover C libraries, system tools, or arbitrary Homebrew formulae
How multi-prefix works
stout introduces the concept of a prefix — an isolated directory that acts as a complete, independent package installation root. Each prefix has its own Cellar, its own bin, lib, include, and share directories, and its own set of installed packages at specific versions.
Create a new prefix for a project:
stout prefix create --name myproject --path ~/projects/myproject
# Created prefix 'myproject' at /Users/you/.stout/prefixes/myproject
# Linked to project directory: ~/projects/myproject
This creates an isolated environment:
~/.stout/prefixes/myproject/
├── bin/
├── include/
├── lib/
├── share/
├── Cellar/
└── .stout-prefix.toml
Install packages into this prefix:
stout install --prefix myproject postgresql@15 [email protected] libpq
# Installing to prefix 'myproject'...
# postgresql@15 15.6 installed
# [email protected] 3.11.8 installed
# libpq 16.2 installed
These packages are completely isolated from your global stout installation and from other prefixes. Your global prefix can have PostgreSQL 16 while this project prefix has PostgreSQL 15 — no conflict, no unlinking, no confusion.
Activating a prefix
To use a prefix, activate it in your current shell:
stout prefix activate myproject
# Activated prefix 'myproject'
# PATH, LIBRARY_PATH, and PKG_CONFIG_PATH updated
Activation prepends the prefix’s bin to your PATH and sets library-related environment variables so that compilers and build tools find the prefix’s libraries first:
# After activation
which psql
# /Users/you/.stout/prefixes/myproject/bin/psql
psql --version
# psql (PostgreSQL) 15.6
# Your global prefix still has PostgreSQL 16
stout prefix deactivate
which psql
# /opt/homebrew/bin/psql (or your global stout bin)
Automatic activation with directory hooks
stout can automatically activate a prefix when you cd into a project directory. Place a .stout-prefix file in your project root:
echo "myproject" > ~/projects/myproject/.stout-prefix
Then enable automatic activation in your shell configuration:
# Add to ~/.zshrc or ~/.bashrc
eval "$(stout prefix shell-hook)"
Now, whenever you change into the project directory, the correct prefix activates automatically:
cd ~/projects/myproject
# stout: activated prefix 'myproject'
cd ~/projects/other-project
# stout: activated prefix 'other-project'
cd ~
# stout: deactivated prefix
This works similarly to tools like direnv or nvm’s automatic version switching, but applies to the entire package set — not just a single runtime.
Prefix-scoped Brewfiles
Each prefix can have its own Brewfile that declaratively specifies which packages it needs:
# ~/projects/myproject/Brewfile
brew "postgresql@15"
brew "[email protected]"
brew "libpq"
brew "redis"
brew "jq"
Install the full environment in one command:
stout bundle install --prefix myproject --file ~/projects/myproject/Brewfile
# Installing 5 packages to prefix 'myproject'...
# All packages installed
This makes setting up a new developer’s machine trivial. Clone the repository, run stout bundle install, and the exact package versions are available.
How isolation works internally
Each prefix is a fully independent Cellar. stout manages symlinks within the prefix’s own bin, lib, and include directories, never touching the global prefix. When a package is installed to a prefix, its bottles are extracted to that prefix’s Cellar and linked within that prefix:
~/.stout/prefixes/myproject/
├── Cellar/
│ ├── postgresql@15/
│ │ └── 15.6/
│ │ ├── bin/psql
│ │ ├── lib/libpq.dylib
│ │ └── ...
│ └── [email protected]/
│ └── 3.11.8/
│ ├── bin/python3.11
│ └── ...
├── bin/
│ ├── psql -> ../Cellar/postgresql@15/15.6/bin/psql
│ └── python3.11 -> ../Cellar/[email protected]/3.11.8/bin/python3.11
└── lib/
├── libpq.dylib -> ../Cellar/postgresql@15/15.6/lib/libpq.dylib
└── ...
Packages installed in one prefix have no visibility into another prefix. Dependency resolution within a prefix only considers packages installed in that prefix, so there is no risk of accidentally linking against a globally installed library.
Sharing packages across prefixes
Installing the same package in multiple prefixes does not duplicate disk usage. stout uses content-addressed storage with hard links (on file systems that support them) or copy-on-write clones (on APFS). When two prefixes both need jq 1.7.1, the bottle is stored once and hard-linked into both Cellar directories:
stout prefix list --verbose
# PREFIX PACKAGES SIZE (unique) SIZE (shared)
# myproject 12 340 MB 890 MB
# other-project 8 210 MB 720 MB
# Total on disk: 480 MB (780 MB saved via dedup)
Managing prefixes
List, inspect, and clean up prefixes with standard subcommands:
# List all prefixes
stout prefix list
# NAME PACKAGES LAST ACTIVATED
# myproject 12 2 hours ago
# other-project 8 3 days ago
# legacy-api 5 2 weeks ago
# Show details for a prefix
stout prefix info myproject
# Prefix: myproject
# Path: /Users/you/.stout/prefixes/myproject
# Packages: 12
# Size: 890 MB (340 MB unique)
# Created: 2026-03-15
# Brewfile: ~/projects/myproject/Brewfile
# Remove a prefix and all its packages
stout prefix delete legacy-api
# Deleted prefix 'legacy-api' (5 packages, 210 MB freed)
CI/CD with prefixes
In CI pipelines, prefixes enable hermetic builds where each job has exactly the packages it needs:
# .github/workflows/build.yml
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install stout
run: curl -fsSL https://stout.neullabs.com/install.sh | sh
- name: Create project prefix
run: |
stout prefix create --name ci-build
stout bundle install --prefix ci-build --file Brewfile
- name: Build with prefix
run: |
eval "$(stout prefix activate ci-build --print-env)"
make build
The prefix ensures the CI environment has exactly the same package versions as development, regardless of what else is installed on the runner.
Comparison with other isolation approaches
| Approach | Scope | Overhead | Homebrew compatible |
|---|---|---|---|
| Docker | Full OS | High (VM/container) | N/A |
| nix-shell | Full package set | Medium (Nix store) | No |
| asdf/mise | Runtimes only | Low | No |
brew link/unlink | Single package | None | Yes |
| stout multi-prefix | Full package set | None | Yes |
stout’s multi-prefix approach provides nix-shell-like isolation without leaving the Homebrew ecosystem. Your formulae, bottles, and taps remain the same — only the installation target changes.
Need Rust performance engineering or AI agent expertise?
Neul Labs — the team behind stout — consults on Rust development, performance optimization, CLI tool design, and AI agent infrastructure. We build fast, reliable systems that ship.