Creating Reproducible Environments with Lock Files
Use stout lock and stout bundle to create deterministic, reproducible package environments for your team.
The reproducibility problem
Software teams frequently encounter a frustrating scenario: a build works on one developer’s machine but fails on another. The root cause is often subtle differences in installed tool versions. Developer A has jq 1.7.1 while Developer B has jq 1.6. The CI server has yet another version. These mismatches lead to wasted debugging time and eroded trust in the build process.
stout solves this with lock files. A lock file pins every package and every transitive dependency to an exact version with a verified hash, ensuring that every machine in your organization runs the same tools down to the byte.
Writing a Brewfile
Start by declaring your project’s system dependencies in a Brewfile at the root of your repository:
# Brewfile - system-level dependencies for the acme-web project
# Build tools
brew "cmake"
brew "ninja"
brew "pkg-config"
# Languages and runtimes
brew "node@20"
brew "[email protected]"
brew "[email protected]"
# Databases (for local development)
brew "postgresql@16"
brew "redis"
# CLI utilities
brew "jq"
brew "yq"
brew "ripgrep"
brew "fd"
brew "gh"
# macOS applications (optional, for local dev)
cask "docker"
cask "kitty"
This file is human-readable and version-controlled. It serves as the source of truth for what system packages your project needs.
Generating a lock file
From the directory containing your Brewfile, generate a lock file:
stout bundle lock
This resolves every package and its dependencies, fetches the current SHA-256 hashes from the index, and writes stout.lock:
# stout.lock
# Auto-generated by stout 1.4.0. Do not edit manually.
# Regenerate with: stout bundle lock
[metadata]
generated_at = "2026-03-22T14:30:00Z"
stout_version = "1.4.0"
brewfile_hash = "sha256:9f8e7d6c5b4a..."
[[packages]]
name = "cmake"
version = "3.31.2"
sha256 = "a1b2c3d4e5f6..."
bottle.arm64_sonoma = "https://index.stout.dev/bottles/cmake-3.31.2.arm64_sonoma.tar.gz"
bottle.x86_64_linux = "https://index.stout.dev/bottles/cmake-3.31.2.x86_64_linux.tar.gz"
dependencies = []
[[packages]]
name = "node"
version = "20.18.1"
sha256 = "b2c3d4e5f6a7..."
bottle.arm64_sonoma = "https://index.stout.dev/bottles/node-20.18.1.arm64_sonoma.tar.gz"
bottle.x86_64_linux = "https://index.stout.dev/bottles/node-20.18.1.x86_64_linux.tar.gz"
dependencies = ["icu4c", "brotli", "c-ares", "libuv", "nghttp2", "openssl@3"]
[[packages]]
name = "openssl@3"
version = "3.4.0"
sha256 = "c3d4e5f6a7b8..."
bottle.arm64_sonoma = "https://index.stout.dev/bottles/[email protected]_sonoma.tar.gz"
bottle.x86_64_linux = "https://index.stout.dev/bottles/[email protected]_64_linux.tar.gz"
dependencies = ["ca-certificates"]
# ... (all other packages and transitive dependencies)
Commit both Brewfile and stout.lock to your repository.
Installing from a lock file
When a teammate clones the repository, they install the exact locked versions:
stout bundle install --frozen
The --frozen flag enforces strict lock file adherence:
- If a package in the lock file is missing from the index, the install fails with a clear error.
- If the SHA-256 hash of a downloaded bottle does not match the lock file, the install fails.
- If the Brewfile has changed since the lock was generated, the install fails and asks you to re-lock.
For less strict environments (like local development), omit --frozen:
stout bundle install
This installs from the lock file when available but allows fallback to latest-compatible versions for packages not in the lock.
Updating locked dependencies
When you want to upgrade dependencies, update the lock file:
# Update all packages to their latest compatible versions
stout bundle lock --update
# Update specific packages only
stout bundle lock --update cmake node@20
# Update with a constraint
stout bundle lock --update "openssl@3>=3.4.1"
After updating, review the changes:
git diff stout.lock
The diff shows exactly which versions changed and which dependencies were affected. Commit the updated lock file once you have verified the new versions work.
Multi-platform lock files
If your team uses both macOS and Linux, the lock file handles both platforms. Each package entry includes bottle URLs for every supported platform:
[[packages]]
name = "ripgrep"
version = "14.1.1"
sha256.arm64_sonoma = "a1b2c3d4..."
sha256.x86_64_sonoma = "b2c3d4e5..."
sha256.x86_64_linux = "c3d4e5f6..."
bottle.arm64_sonoma = "https://index.stout.dev/bottles/ripgrep-14.1.1.arm64_sonoma.tar.gz"
bottle.x86_64_sonoma = "https://index.stout.dev/bottles/ripgrep-14.1.1.x86_64_sonoma.tar.gz"
bottle.x86_64_linux = "https://index.stout.dev/bottles/ripgrep-14.1.1.x86_64_linux.tar.gz"
When stout bundle install --frozen runs, it selects the bottle matching the current platform and verifies the platform-specific hash.
To explicitly generate a lock file covering specific platforms:
stout bundle lock --platforms arm64_sonoma,x86_64_linux
Team workflows
Onboarding a new developer
Add this to your project’s setup instructions:
# 1. Install stout
curl -fsSL https://get.stout.dev | sh
eval "$(stout shellenv zsh)"
# 2. Install all system dependencies from the lock file
stout bundle install --frozen
# 3. Proceed with the rest of setup
npm install # or your language-specific package manager
make setup
New developers get the exact same tool versions as everyone else, eliminating the “works on my machine” class of issues on day one.
Pull request workflow
When a dependency update is needed, the process looks like this:
# Create a branch for the update
git checkout -b update-system-deps
# Edit the Brewfile if adding or removing packages
vim Brewfile
# Regenerate the lock file
stout bundle lock
# Test the new environment
stout bundle install --frozen
make test
# Commit and push
git add Brewfile stout.lock
git commit -m "Update system dependencies"
git push origin update-system-deps
Reviewers can inspect the stout.lock diff to see exactly what changed. CI runs stout bundle install --frozen to verify the lock file is valid and the tests pass with the new versions.
Auditing installed packages
Generate a report of all locked packages and their licenses:
stout bundle audit --file stout.lock
This outputs a table of packages, versions, licenses, and known security advisories:
PACKAGE VERSION LICENSE ADVISORIES
cmake 3.31.2 BSD-3-Clause none
node 20.18.1 MIT none
openssl@3 3.4.0 Apache-2.0 CVE-2024-XXXX (low)
postgresql@16 16.6 PostgreSQL none
redis 7.4.1 BSD-3-Clause none
...
Environment isolation with stout env
For projects that need strict isolation (avoiding conflicts between projects that need different versions of the same tool), use stout env:
# Create an isolated environment for this project
stout env create --name acme-web --file Brewfile
# Activate the environment
stout env activate acme-web
# Now all stout-managed tools resolve from this environment
which node
# /usr/local/stout/envs/acme-web/bin/node
node --version
# v20.18.1
# Deactivate when done
stout env deactivate
Each environment has its own prefix, so acme-web can use node@20 while another project uses node@22 without conflict.
Integrate with direnv for automatic activation:
# .envrc (in your project root)
eval "$(stout env activate acme-web --shell-hook)"
Now entering the project directory automatically activates the correct stout environment, and leaving it restores your default tools.
Lock file best practices
-
Always commit your lock file. The lock file is not generated output to ignore; it is a critical piece of your build specification.
-
Use
--frozenin CI. Local development can be more flexible, but CI should always enforce the lock file. -
Update lock files in dedicated PRs. Mixing dependency updates with feature changes makes it harder to bisect problems.
-
Review lock file diffs. Treat
stout.lockchanges with the same scrutiny as code changes. Unexpected version bumps in transitive dependencies can introduce subtle issues. -
Re-lock periodically. Set a monthly reminder or automated PR (using Dependabot-style automation) to keep dependencies current and catch security updates.
-
Pin major versions in Brewfile, let the lock file handle exact versions. Write
brew "node@20"in your Brewfile, notbrew "[email protected]". The Brewfile expresses your compatibility range; the lock file captures the precise resolution.
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.