A version-locking wrapper for Homebrew that maintains a brew.lock file containing exact versions of all installed packages, enabling reproducible installations across machines.
- Transparent wrapper: Alias
brewlocktobrewand use Homebrew normally - Automatic version tracking: Every install/uninstall/upgrade updates
brew.lock - Version-locked bundle install: Install packages with version verification
- Full package support: Tracks formulae, casks, taps (with commit SHAs), and Mac App Store apps
- JSONC format: Human-readable lock file with comment support
- Custom tap URLs: Tracks custom tap repository URLs for reproducibility
brew tap shipworthyai/tap
brew install brewlockNote: The Homebrew formula provides Apple Silicon macOS and x86_64 Linux binaries.
If you want to build from source or contribute to development:
# Clone the repository
git clone https://github.com/ShipWorthyAI/brewlock.git
cd brewlock
# Install dependencies
bun install
# Build the binary
bun run build
# Or link for development
bun linkAdd this alias to your shell configuration to use brewlock as a transparent wrapper for Homebrew:
# For zsh (add to ~/.zshrc):
alias brew='brewlock'
# For bash (add to ~/.bashrc):
alias brew='brewlock'
# For fish (add to ~/.config/fish/config.fish):
alias brew 'brewlock'After adding the alias, restart your shell or run source ~/.zshrc (or equivalent).
# Install brewlock
brew tap shipworthyai/tap
brew install brewlock
# Set up the alias (add to your ~/.zshrc)
echo "alias brew='brewlock'" >> ~/.zshrc
source ~/.zshrc
# Generate a lock file from your current packages
brew lock
# Use brew normally - brewlock tracks versions automatically
brew install git
brew install --cask dockerAll standard brew commands work transparently:
brew install git # Installs git and updates brew.lock
brew install --cask docker # Installs Docker and updates brew.lock
brew upgrade # Upgrades all and regenerates brew.lock
brew uninstall node # Removes node and updates brew.lockbrew lock # Generate brew.lock from currently installed packages
brew check # Verify installed versions match brew.lock
brew help # Show help message (or brewlock help for brewlock-specific help)brew bundle install # Installs from brew.lock with version checking# Option 1: Point BREWLOCK directly to your dotfiles (recommended)
# Add to your shell config (~/.zshrc or ~/.bashrc):
export BREWLOCK="$HOME/dotfiles/brew.lock"
# Option 2: Copy (or symlink) the lock file to your dotfiles
cp ~/brew.lock ~/dotfiles/
cd ~/dotfiles && git add brew.lock && git commit -m "Update brew.lock"
# On a new machine: restore from brew.lock
cp ~/dotfiles/brew.lock ~/brew.lock
brew bundle installBy default, the lock file is stored at ~/brew.lock (in your home directory). You can customize this location using the BREWLOCK environment variable:
# Set a custom lock file path
export BREWLOCK=/path/to/your/brew.lock
# Example: Store in your dotfiles repo
export BREWLOCK="$HOME/dotfiles/brew.lock"Performance tracing is enabled by default and writes newline-delimited JSON
spans to ~/.brewlock/performance.ndjson. This is useful when comparing
updateLockFile runs before and after optimization work:
# Disable tracing for one command
BREWLOCK_PERF=0 brew install git
# Choose a log path for a measurement run
BREWLOCK_PERF_LOG=/tmp/brewlock-perf.ndjson brew upgradeEach span includes timing, parent span IDs, command metadata, exit codes, and lock-file entry counts where applicable.
Direct-git tap metadata is the default. To compare against Homebrew's
brew tap-info --json path, override the backend:
BREWLOCK_TAP_INFO_BACKEND=brew brew lockLeave BREWLOCK_TAP_INFO_BACKEND unset to use the default direct-git backend.
Local formula metadata and version lookups are the default. To compare against
Homebrew's brew info --json=v2 --installed and
brew info --json=v2 <formula> paths, override the backend:
BREWLOCK_FORMULA_INFO_BACKEND=brew brew lockLeave BREWLOCK_FORMULA_INFO_BACKEND unset to use the default local backend.
Local cask metadata and version lookups are the default. To compare against
Homebrew's brew info --cask --json=v2 --installed and
brew info --cask --json=v2 <cask> paths, override the backend:
BREWLOCK_CASK_INFO_BACKEND=brew brew lockLeave BREWLOCK_CASK_INFO_BACKEND unset to use the default local backend.
Generated lock files include a warning header followed by a JSONC body organized by package type:
| Field | Description |
|---|---|
url |
Custom GitHub repo URL (for non-official taps) |
commit |
Git commit SHA for reproducibility |
official |
Whether this is an official Homebrew tap |
| Field | Description |
|---|---|
version |
Linked (active) version |
installed |
All installed versions in Cellar (if multiple) |
revision |
Formula revision (e.g., distinguishes 2.43.0 from 2.43.0_1) |
tap |
Source tap (e.g., homebrew/core) |
pinned |
Whether the formula is pinned |
dependencies |
Direct dependencies |
sha256 |
Platform-specific bottle SHA256 for verification |
installed_as_dependency |
Whether installed as a dependency of another package |
installed_on_request |
Whether user explicitly installed it |
| Field | Description |
|---|---|
version |
Installed cask version |
tap |
Source tap (e.g., homebrew/cask) |
sha256 |
Download SHA256 for verification |
auto_updates |
Whether the app self-updates |
| Field | Description |
|---|---|
id |
App Store ID (required for installation) |
version |
Installed app version |
- Command Interception: brewlock parses brew commands to detect package modifications
- Passthrough Execution: The actual brew command is executed with live output streaming
- Lock File Update: After successful commands, brewlock queries installed versions and updates
brew.lock - Bundle Install: When running
brew bundle install, brewlock checks versions and warns about mismatches
Homebrew doesn't natively support exact version installation for all packages. brewlock handles this pragmatically:
- Versioned formulae: Uses
brew install package@versionsyntax when available (e.g.,python@3.11) - Current version match: Skips installation if the installed version matches the lock file
- Version mismatch: Warns when the installed version differs from the lock file
- Casks: Tracks versions but casks are typically pinned by their formula
# Install dependencies
bun install
# Run tests
bun test
# Lint
bun run lint
# Type check
bun run typecheck
# Build binary
bun run build
# Verify the compiled CLI with a stubbed Homebrew
bun run test:binary
# Run the CLI directly (without building)
bun run src/index.ts --help
# Build release tarball
bun run build:releaseBoth build commands apply and verify an ad hoc signature on macOS after compilation. CI checks the compiled CLI's help, command forwarding, output, exit status, and preservation of the lock file. Release builds run those checks before packaging and again after extracting the tarball.
brewlock/
src/
index.ts # CLI entry point
parser.ts # Command parser
executor.ts # Brew command executor
lock-manager.ts # Lock file read/write (JSONC format)
version-resolver.ts # Version querying
bundle-install.ts # Bundle install with versions
types.ts # Shared type definitions
tests/
mocks/ # Test mocking utilities
*.test.ts # Test files
package.json
tsconfig.json
This project is open source and available under the MIT License.
Created by ShipWorthy β΅οΈ
