How malt is built and what it guarantees. For installing and using malt, see the README.
malt's behaviour follows from a small number of design choices - each one a direct consequence of wanting safe concurrency and interruption survival.
malt installs to /opt/malt and never touches Homebrew. The path is short on purpose: Mach-O load command patching needs room to replace the original Homebrew path in-place, and /opt/malt always fits.
/opt/malt/
├── store/ # Content-addressable bottle storage (immutable, by SHA256)
├── Cellar/ # Installed kegs (APFS cloned from store/)
├── Caskroom/ # Installed cask applications
├── opt/ # Versioned formula symlinks
├── bin/ # Symlinks to keg binaries
├── lib/ # Symlinks to keg libraries
├── include/ # Symlinks to keg headers
├── share/ # Symlinks to keg shared data
├── tmp/ # In-progress downloads and extractions
├── cache/ # Cached API responses (TTL-based)
└── db/ # SQLite database + advisory lock
Bottles are stored by their SHA256. The same bottle is never downloaded or extracted twice; multiple installed kegs reference the same store entry. Store entries are immutable - only mt purge --store-orphans removes them. Kegs in Cellar/ are materialized via APFS clonefile(), which creates a copy-on-write clone at zero disk cost; non-APFS volumes fall back to a recursive copy.
This is what makes mt rollback an instant operation: every previously installed bottle is still in the store, so reverting is unlink → re-clone → DB update, with no re-download.
Each bottle download is a single-pass pipeline:
Network (HTTPS from GHCR CDN)
├──► SHA256 hasher (streaming - computed as chunks arrive)
└──► gzip/zstd decompressor
└──► tar extractor
└──► filesystem write to tmp/
No intermediate archive file is written to disk. The SHA256 is verified against the Homebrew API manifest immediately after the stream completes; on mismatch, the extracted directory is deleted before any commit happens.
Homebrew bottles contain hardcoded /opt/homebrew/Cellar/... paths in Mach-O load commands. malt corrects them in four steps:
- Parse headers with struct-aware parsing (not raw byte scanning).
- Identify every relevant load command (
LC_ID_DYLIB,LC_LOAD_DYLIB,LC_RPATH, etc.). - Rewrite paths in-place and pad the remaining space with null bytes.
- On arm64, ad-hoc codesign the patched binary via
codesign --force --sign -.
Text files (.pc configs, shell scripts) containing @@HOMEBREW_PREFIX@@ or @@HOMEBREW_CELLAR@@ placeholders are patched the same way. Patching always happens on the Cellar copy, never the store original - if it fails, the Cellar copy is deleted and the store entry stays pristine for retry.
Most alternative clients stop once the files are in place. malt also runs the configuration a package declares, in both forms Homebrew supports.
Homebrew v6 introduced a declarative post_install_steps array for formulae, and homebrew-core now uses it throughout. malt runs those steps natively - across install, upgrade, and migrate. A formula that declares steps is configured by its steps alone; its Ruby post_install, if any, is not run.
Casks declare the same step schema as preflight_steps / postflight_steps / uninstall_preflight_steps / uninstall_postflight_steps (Homebrew v7). mt install --cask runs the preflight over the staged artefact before anything is placed and the postflight once the cask is recorded; mt uninstall, mt upgrade and mt rollback run the steps stored at install time, so they match the version on disk, and drop the symlinks a cask declared for removal. Cask steps are confined to the Caskroom, the malt prefix, $HOME/Library and the applications directory, and may never remove or relocate those roots, their top-level directories, or anything under Keychains, Mail, Messages, Safari, Accounts, Mobile Documents and CloudStorage; steps that need sudo are reported and skipped, never escalated. terminate_process and delete_keychain_certificate act on the user session as upstream defines them and are not confined. A failed uninstall preflight keeps the cask on disk; mt uninstall --force continues past it. mt install --dry-run lists the steps a cask would run and which ones malt refuses; mt upgrade --dry-run does not, and mt uninstall --dry-run stops before them.
Homebrew 7 deprecates the Ruby post_install hook in favour of post_install_steps, but still runs it. homebrew-core has migrated; many third-party taps have not. For those formulae malt tries its native interpreter first. It parses and evaluates the Ruby subset those blocks actually use:
Pathnameoperations,FileUtils,inreplace,Dir.glob- string interpolation,
%w[]arrays, the boolean operators - control flow:
if/unless,.each/.select/.map Formula["name"]cross-lookup,ENVaccess
Source for homebrew-core formulas is fetched on demand from GitHub if the tap isn't cloned locally.
Every mutating filesystem operation - write, rm, chmod, symlink - is validated against the formula's Cellar prefix and the malt prefix; paths containing .. or resolving outside the sandbox via symlinks are rejected immediately.
When the interpreter hits an unsupported construct, the user is directed to --use-system-ruby, which delegates to a sandboxed Ruby subprocess scoped to the formula's cellar, with:
- a scrubbed environment
RLIMIT_CPU/AS/FSIZEcaps- terminal escape sequences filtered from child output
Formula declares post_install_steps?
│
├── yes → run the declarative steps natively → done
│
└── no → Formula has a Ruby post_install?
│
├── yes → Try native DSL interpreter
│ │
│ ├── success → done (package fully configured)
│ │
│ └── unsupported construct → --use-system-ruby set?
│ │
│ ├── yes → delegate to sandboxed Ruby subprocess
│ └── no → skip with clear message
│
└── no → done (no post-install needed)
Every install follows nine steps. Failure at any step triggers cleanup of that step only - no prior state is modified.
- Acquire lock - exclusive advisory lock on
db/malt.lock - Pre-flight - resolve dependencies, check disk space, detect link conflicts
- Download - fetch bottles from GHCR CDN with streaming SHA256 verification
- Extract - decompress and untar to
tmp/ - Commit to store - atomic rename from
tmp/tostore/ - Materialize - APFS clonefile from
store/toCellar/, patch Mach-O, codesign - Link - create symlinks in
bin/,lib/, etc., record in DB - DB commit - insert into kegs, dependencies, links tables in a single transaction
- Release lock - clean up tmp files
Upgrades follow the same protocol on the new version before anything is removed from the old; on failure, the old symlinks are restored. Read-only commands (list, info, search) do not acquire the lock.
malt's correctness rests on a few load-bearing properties:
- SHA256 verification. Streaming hash computed during download, verified before extraction. No unverified data touches the store.
- Tar entry pre-scan. Every entry's name and symlink target are validated before any byte is written. The 512-byte tar header is checksum-verified per entry. Hardlinks are applied via
linkat(..., 0), which refuses to follow a symlink - so a hostile tarball cannot land a hardlink inside the keg via a symlink to/etc/passwd. - Pre-flight checks. Dependencies resolved, disk space verified, link conflicts detected before any download begins.
- Atomic installs. The 9-step protocol uses
errdeferat every stage. Interrupted installs leave no partial state. - Concurrent access. A 30-second-timeout advisory file lock prevents concurrent mutations. Read-only commands don't acquire it.
- Upgrade rollback. New version is fully installed and verified before the old version is touched.
- Store immutability. Store entries are never modified after commit. Patching happens on the Cellar clone.
- Mach-O parser hardening. Section offsets and string-table indices are validated against the slice using overflow-checked arithmetic, so a bottle with crafted load commands can't wrap an integer into a bounds-bypass.
- DSL path sandboxing. Every mutating operation in the post_install interpreter is validated against the Cellar/malt prefix;
..and symlink-escape paths are rejected. - DSL
systemis argv-only. The interpreter'ssystembuiltin spawns with an argv slice and pins the executable - never/bin/sh -c, never PATH-resolved. A formula that writessystem "rm", argcannot reach the parent shell.
The supply-chain story:
- Signed releases. Every release is cosign-signed keyless via GitHub OIDC;
install.shverifies the signature before trusting the SHA256 checksum. A leaked GitHub token is not enough to ship a malicious malt binary. - Pinned third-party source.
homebrew-coreand third-party taps are pinned to a specific commit SHA. Formula Ruby source is SHA256-verified against an embedded manifest at that commit. A rewritten upstream branch cannot substitute a formula's bottle URL mid-install. Advance a tap pin explicitly withmt tap --refresh user/repo. - Sandboxed
post_install. The opt-in--use-system-rubypath runs inside asandbox-execprofile scoped to the formula's cellar. Hostile formulas can affect their own install prefix and nothing else. - Boundary validation.
MALT_PREFIX,MALT_CACHE, launchd service declarations, install-script checksums, and HTTP redirects fail-closed on malformed or suspicious input - no silent HTTPS→HTTP downgrades, no/bin/shin service argv, no..in prefix paths. A cask that declares nosha256is refused rather than treated as opted out; only an API cask's explicitsha256 :no_checkskips verification. Tap and local.rbpackages must pin a 64 lowercase-hexsha256; one that declaressha256 :no_checkinstalls only with--allow-unpinned, with a warning, and never as a.pkg. A manifest URL must behttps://unless a digest already pins the bytes it returns - so the handful of upstream packages still served over plaintext keep installing, while one that hash-verifies nothing is refused outright. - Trusted verifier.
mt version updaterefuses acosignthat resolves inside/opt/malt, which packages can write to. A shim dropped there cannot rubber-stamp a malicious update. - Posture visibility.
mt doctorflags world- or group-writable paths and unexpected ownership under/opt/malt, so multi-user machines see their attack surface at a glance.
malt's binary is small because it ships only five subsystems and the glue between them:
- SQLite. ACID writes, reverse-dependency queries, linker-conflict detection, atomic rollback after a failed upgrade. Survives
kill -9mid-write. - Native post-install. A steps executor for Homebrew's declarative steps, plus a Ruby-subset interpreter in Zig for the taps that still ship
post_installblocks. - Mach-O patching with arm64 ad-hoc codesign. Rewrites
/opt/homebrew→MALT_PREFIXand re-signs sodyldloads the result on modern macOS. - Install lock.
flockondb/malt.lockplus a symlink-tree walk, acquired by every mutating command, so two invocations - or a Ctrl-C'd install - can't corrupt state. sandbox-execprofile. The opt-in--use-system-rubypath runs formula scripts in a deny-default sandbox (caps and escape-filtering as above).
All five run per-install. The warm install times in the benchmarks are their combined wall-clock cost.
The interactive dashboard (mt tui) is the one piece that doesn't run per-install - it's compiled into the same binary instead of shipping as a companion tool, and costs only about 300 KB to keep there.