A fast package manager for macOS and Linux. Written in Zig. Native install pipeline for the top 100 Homebrew formulae and top 100 casks (no brew, no Ruby), with verified Homebrew fallback for everything else and native .deb support for Linux/Docker.
- Fast warm installs — already-installed no-ops return in milliseconds (5.8ms on the v0.1.192 sandboxed
yt-dlpbenchmark) - Parallel downloads — all dependencies download and extract at the same time
- No Ruby runtime — single static binary, instant startup
- No auto-update —
nb installjust installs; self-update is explicit vianb update - No quarantine — cask installs skip
com.apple.quarantine, so apps open without Gatekeeper prompts - Native installs — top 100 formulae and top 100 casks install without Homebrew, Ruby, or
brewsubprocess (v0.1.192) - Third-party taps —
nb install user/tap/formulajust works. The only fast Homebrew client with tap support - Drop-in Homebrew replacement — same formulas, same bottles, same casks
- Linux + Docker — native .deb support, up to 13x faster than apt-get on warm installs
Homebrew is great software and powers millions of dev machines. nanobrew makes different tradeoffs:
| Homebrew | nanobrew | |
|---|---|---|
| Auto-update | brew install runs brew update first (can take minutes) |
nb install just installs. Self-update is explicit via nb update. |
| Gatekeeper quarantine | Casks get com.apple.quarantine — triggers "Are you sure?" dialog |
No quarantine flag — apps open immediately |
| Parallel downloads | Sequential by default; set HOMEBREW_DOWNLOAD_CONCURRENCY to change |
All dependencies download simultaneously out of the box |
| Runtime | Ruby (~57 MB) | Single 1.2 MB static binary. Instant startup, no bootstrapping. |
| Brewfile no-ops | brew bundle rechecks everything (~10s even when satisfied) |
nb bundle install returns instantly when nothing to do |
If you rely on post_install hooks, build-from-source options, or Mac App Store integration, Homebrew is still the right choice. nanobrew covers the fast path: bottles, casks, and bundles.
| Package | Homebrew | zerobrew (cold) | zerobrew (warm) | nanobrew (cold) | nanobrew (warm) |
|---|---|---|---|---|---|
| tree (0 deps) | 4.070s | 1.254s | 0.242s | 0.507s | 0.009s |
| ffmpeg (11 deps) | 14.252s | 3.986s | 2.147s | 1.624s | 0.287s |
| wget (6 deps) | 3.935s | 5.502s | 0.587s | 3.211s | 0.027s |
Benchmarks on Apple Silicon (GitHub Actions macos-14), 2026-03-23. Auto-updated weekly.
| nanobrew | zerobrew | Homebrew | |
|---|---|---|---|
| Binary size | 1.2 MB | 7.9 MB | 57 MB (Ruby runtime) |
nanobrew is 6.8x smaller than zerobrew and 47x smaller than Homebrew. See how these are measured in the benchmark workflow.
nanobrew's --deb mode is a full apt-get replacement: fetches APT package indices, resolves dependencies, downloads and extracts .deb files — all in pure Zig with no subprocess calls.
| Package set | Deps | apt-get | nanobrew (warm) | Speedup |
|---|---|---|---|---|
| curl wget | 35 | 3,426ms | 448ms | 7.6x |
| curl wget tree jq htop tmux | 53 | 3,584ms | 521ms | 6.9x |
| git vim build-essential | 116 | 43,833ms | 3,402ms | 12.9x |
| nginx redis-server postgresql-client | 78 | 5,501ms | 1,402ms | 3.9x |
Verified benchmarks on Ubuntu 24.04.4 LTS (aarch64, Docker/Colima), median of 3 runs. Warm = NBIX binary index cache + cached .deb blobs,
--skip-postinst. Seebench/for reproduction.
What makes it fast:
- NBIX binary index cache — 70K packages deserialized in 32ms (vs 3s HTTP + 72MB gzip decompress + text parse)
- 8-thread parallel .deb downloads with HTTP connection reuse
- 8-thread parallel extraction — concurrent ar/gzip/tar parsing via native Zig tar
- Arena allocator — single
deinit()frees all 70K parsed packages
# One-liner
curl -fsSL https://nanobrew.trilok.ai/install | bash
# Or via Homebrew
brew tap justrach/nanobrew https://github.com/justrach/nanobrew
brew install nanobrew
# Or build from source (needs Zig 0.16.0+)
git clone https://github.com/justrach/nanobrew.git
cd nanobrew && ./install.sh# v0.1.193 and later: self-update works in one call
nb updateIf you're on v0.1.192 or older and nb update errors with
could not download SHA256 checksum, the self-updater on your installed
binary can't reach past a redirect-chain bug in its native HTTP client
(fixed in v0.1.193). Re-run the installer once to get unstuck, then
nb update will work for all future releases:
curl -fsSL https://nanobrew.trilok.ai/install | bashThe same bug also affected exactly v0.1.190 via a different code path (file-naming bug in the extract step); the same one-liner unsticks it.
nb install tree # install a package
nb install ffmpeg wget curl # install multiple at once
nb install --shims yt-dlp # expose yt-dlp, keep dependency tools private
nb remove tree # uninstall
nb list # see what's installed
nb info jq # show package details
nb search ripgrep # search formulas and casksnb install --shims yt-dlpShimmed installs are an experimental link mode for packages whose dependencies ship command-line tools you do not want exposed globally. The requested formula gets wrapper shims in /opt/nanobrew/prefix/bin; dependency executables are kept out of prefix/bin and are only added to that wrapper's private PATH. This keeps commands like deno or python available to the requested tool without making those dependency executables first-class shell commands. You can also enable this mode for formula installs with NANOBREW_SHIMS=1.
nb install steipete/tap/sag # install from a third-party tap
nb install indirect/tap/bpb # taps with bottles work toonanobrew fetches the Ruby formula directly from GitHub, parses it, and installs — no brew tap step needed. Supports bottles, source builds, and pre-built binaries.
nb install --cask firefox # install a .dmg/.pkg/.zip app
nb remove --cask firefox # uninstall it
nb upgrade --cask # upgrade all casksAs of v0.1.192, the top 100 casks install through nanobrew's native pipeline — no brew subprocess, no Homebrew prefix, no Ruby. Native cask support covers apps, .pkg, fonts, binaries, suites, copied artifacts, installer scripts, .tar.xz, and extensionless vendor URLs. Casks outside the top 100 still fall back to the verified Homebrew path.
nb install --deb curl wget git # install from Ubuntu/Debian repos
nb remove --deb curl # remove a deb package
nb upgrade --deb # upgrade all installed deb packages
nb list # shows deb packages alongside brew packages
nb outdated # checks deb packages for newer versions too# Replace slow apt-get in Dockerfiles
COPY --from=nanobrew/nb /nb /usr/local/bin/nb
RUN nb init && nb install --deb curl wget git- Auto-detects distro and architecture (Ubuntu/Debian, amd64/arm64)
- Resolves virtual packages via
Provides:field (e.g.build-essentialworks) - Picks the best alternative when multiple packages satisfy a dependency
- Runs
postinstscripts andldconfigso shared libraries work out of the box - Tracks installed files in
state.jsonfor clean removal - Content-addressable cache — warm installs are instant
nb outdated # see what's behind
nb upgrade # upgrade everything
nb upgrade tree # upgrade one package
nb pin tree # prevent a package from upgrading
nb unpin tree # allow upgrades againnb rollback tree # revert to the previous version
nb bundle dump # export installed packages to a Nanobrew file
nb bundle install # reinstall everything from a Nanobrew filenb doctor # check for common problems
nb cleanup # remove old caches and orphaned files
nb cleanup --dry-run # see what would be removed firstnb telemetry status
nb telemetry off
nb telemetry onnanobrew sends anonymized, best-effort download timing events to https://backend.trilok.ai/v1/telemetry/system. This helps prioritize which packages and casks should get native nanobrew support first, based on real download/install demand and slow paths.
The exact event shape is:
{
"schema": 1,
"source": "nanobrew",
"event": "download",
"os": "macos",
"arch": "arm64",
"ram_gb": 128,
"cpu_count": 10,
"operation": "download",
"target_kind": "formula",
"target_name": "uv",
"duration_ms": 120,
"download_bytes": 33000000,
"success": true
}It does not send URLs, paths, hostnames, usernames, IPs, user IDs, full package lists, or command history. target_name is only a package-like token such as uv, firefox, or owner/tap/pkg. You can opt out with nb telemetry off, NANOBREW_NO_TELEMETRY=1, or NANOBREW_TELEMETRY=0.
nb deps ffmpeg # list all dependencies
nb deps --tree ffmpeg # show dependency tree
nb services list # show launchctl services from installed packages
nb services start postgresql # start a service
nb services stop postgresql # stop a servicenb completions zsh >> ~/.zshrc
nb completions bash >> ~/.bashrc
nb completions fish > ~/.config/fish/completions/nb.fishnb update # self-update nanobrew
nb init # create directory structure (run once)
nb help # show all commandsnb install ffmpeg # macOS: Homebrew bottles
│
├─ 1. Resolve dependencies (BFS, parallel API calls)
├─ 2. Skip anything already installed (warm path: ~3.5ms)
├─ 3. Download bottles in parallel (native HTTP, streaming SHA256)
├─ 4. Extract into content-addressable store (/opt/nanobrew/store/<sha>)
├─ 5. Clone into Cellar via APFS clonefile (zero-copy, instant)
├─ 6. Relocate Mach-O headers + batch codesign
└─ 7. Symlink binaries into /opt/nanobrew/prefix/bin/
nb install --deb curl # Linux: .deb packages
│
├─ 1. Detect distro from /etc/os-release (Ubuntu/Debian, amd64/arm64)
├─ 2. Fetch + decompress package index (main + universe components)
├─ 3. Build provides map for virtual package resolution
├─ 4. Resolve dependencies (topological sort, index-aware alternatives)
├─ 5. Download .debs with streaming SHA256 verification
├─ 6. Parse ar archive, decompress data.tar natively (zstd/gzip)
├─ 7. Extract to / and track installed files in state.json
├─ 8. Run postinst scripts (ca-certificates, ldconfig, etc.)
└─ 9. Run ldconfig for shared library registration
nb install steipete/tap/sag # Third-party taps
│
├─ 1. Detect tap syntax (user/tap/formula)
├─ 2. Fetch Ruby formula from GitHub (raw.githubusercontent.com)
├─ 3. Parse .rb file (version, url, sha256, deps, bottle blocks)
├─ 4. Resolve dependencies normally (they're homebrew-core names)
└─ 5. Install via bottle or source path (same pipeline as above)
Dependency ordering walks the explicit formula graph and topologically sorts it in O(V+E). The O(1) resolver improvement in v0.1.190 refers to queue dequeue during that sort, not solving arbitrary version constraints.
Key design choices:
- Content-addressable store — deduplicates bottles by SHA256. Reinstalls are instant because the data is already there.
- APFS clonefile — copy-on-write on macOS means no extra disk space when materializing from the store.
- Streaming SHA256 — hash is verified during download, no second pass over the file.
- Native binary parsing — reads Mach-O (macOS) and ELF (Linux) headers directly instead of spawning
otool/patchelf. - Native ar + decompression — .deb extraction without
dpkg,ar, orzstdbinaries. Only needstar. - Single static binary — no runtime dependencies. 1.2 MB.
# Run all tests (macOS — native)
zig build test
# Run individual module tests with verbose output
zig test src/deb/index.zig # 7 tests: package parsing, provides map
zig test src/deb/resolver.zig # 17 tests: dependency resolution, virtual packages
# Cross-compile and run on Linux via Colima/Docker
zig build test -Dtarget=aarch64-linux # cross-compile to static ELF
docker run --rm -v .zig-cache/o/<hash>/test:/test alpine /test
# Or as a one-liner (find the binary automatically)
docker run --rm -v "$(find .zig-cache -name test -newer build.zig | head -1):/t:ro" alpine /tZig's cross-compilation produces a statically-linked binary that runs directly in any Linux container — no need to install Zig or any toolchain inside Docker.
Follow CONTRIBUTING.md for all future issues and PRs.
The short version:
- every PR must be tied to an issue
- every fix must show red-to-green proof
- every non-trivial branch must be rebased onto current
main - PRs over 500 changed lines will usually be rejected unless they are clearly justified, tightly scoped, and good enough to survive strict review
/opt/nanobrew/
cache/
blobs/ # downloaded bottles (by SHA256)
api/ # cached formula metadata (5-min TTL)
tokens/ # GHCR auth tokens (4-min TTL)
tmp/ # partial downloads
store/ # extracted bottles (by SHA256)
prefix/
Cellar/ # installed packages
Caskroom/ # installed casks
bin/ # symlinks to binaries
opt/ # symlinks to keg dirs
db/
state.json # installed package state
nanobrew uses Homebrew's formulas, bottles, and cask definitions. It's a faster client for the same ecosystem — not a fork.
- Bottle installs — all pre-built Homebrew bottles install correctly
- Cask installs —
.dmg,.zip,.pkg, and.tar.gzcasks - Dependency resolution — same transitive deps as Homebrew
- Third-party taps —
nb install user/tap/formulafetches from GitHub - Shared Cellar — packages install to
/opt/nanobrew/prefix/Cellar/(same layout as Homebrew) - Bundle/Brewfile —
nb bundle dumpandnb bundle installfor commonbrew "pkg"andcask "pkg"lines
- Ruby
post_installhooks — Homebrew formulae with Rubypost_installblocks won't run those hooks. Most bottles don't need them. - Build from source with custom options —
args: ["with-feature"]in Brewfiles is ignored tapcommand — nanobrew auto-fetches taps inline; standalonebrew tapis not needed- Mac App Store (
mas) — not supported - Complex Ruby DSL in Brewfiles — conditional blocks, custom Ruby code
nb migrate # scan /opt/homebrew/Cellar and Caskroom, import into nanobrew's DBAfter migration, nb list, nb outdated, and nb upgrade will see your existing packages.
Packages installed by nanobrew live in /opt/nanobrew/prefix/Cellar/ — they don't interfere with Homebrew's /opt/homebrew/Cellar/. You can safely remove nanobrew with nb nuke without affecting Homebrew.
Experimental — works well for common packages. If something breaks, open an issue.
License: Apache 2.0
| Command | Short | What it does |
|---|---|---|
nb install <pkg> |
nb i |
Install packages |
nb install --cask <app> |
Install macOS apps | |
nb install --deb <pkg> |
Install .deb packages (Linux/Docker) | |
nb install user/tap/formula |
Install from a third-party tap | |
nb remove <pkg> |
nb ui |
Uninstall packages |
nb remove --deb <pkg> |
Remove a .deb package (Linux/Docker) | |
nb list |
nb ls |
List installed packages (brew + deb) |
nb leaves [--tree] |
List installed formulae with no dependents | |
nb where <pattern> |
nb wh |
Show installed kegs, prefix files, and index hits matching pattern |
nb info <pkg> |
Show package details | |
nb info --cask <app> |
Show cask details | |
nb search <query> |
nb s |
Search formulas and casks |
nb upgrade [pkg] |
Upgrade packages | |
nb upgrade --deb |
Upgrade all installed .deb packages | |
nb outdated |
List outdated packages (brew + deb) | |
nb pin <pkg> |
Prevent upgrades | |
nb unpin <pkg> |
Allow upgrades | |
nb rollback <pkg> |
nb rb |
Revert to previous version |
nb bundle dump |
Export installed packages | |
nb bundle install |
Import from bundle file | |
nb doctor |
nb dr |
Health check |
nb cleanup |
nb clean |
Remove old caches |
nb deps [--tree] <pkg> |
Show dependencies | |
nb services |
Manage services (launchctl/systemd) | |
nb completions <shell> |
Print shell completions | |
nb telemetry [status|on|off] |
View or change telemetry opt-in | |
nb nuke |
Remove all of nanobrew's state | |
nb migrate |
Import packages from Homebrew | |
nb update |
Self-update nanobrew | |
nb init |
Create directory structure | |
nb help |
Show help |
See CHANGELOG.md for version history.
