* feat(update): recognise Homebrew installs and add mise, go install docs
* fix(update): match the keg shape, correct the docs, cover Apply and cross-target
* test(update): build the release fixture from the running platform
* fix(update): require a Homebrew receipt before refusing self-update
Review found the keg check was shape-only, and it was right: a directory
tree a user built themselves can have exactly a keg's shape. Classifying
one as Homebrew refuses a `zero upgrade` that would have worked
perfectly, on a machine Homebrew has never touched.
Two changes, cheap one first. The shape rule now requires the full
<prefix>/Cellar/<formula>/<version>/bin/<binary> depth rather than one
segment fewer, which already excludes the exact path the review raised.
Then, only for a path that still looks like a keg, one Stat for
INSTALL_RECEIPT.json — the file Homebrew writes into every keg and
nothing else does.
The receipt is what the old comment argued against, on the grounds that
it would put filesystem I/O on a path that runs at every version check.
That is no longer true of it: the Stat happens only after the shape test
has already matched, so an ordinary install still reaches no filesystem
here at all.
A directory named INSTALL_RECEIPT.json is not a receipt, which a bare
Stat would have accepted silently.
NOT taken: the same review asked for the Homebrew branch to be moved
ahead of the cross-target one in upgradeGuidance. That ordering is
deliberate and already pinned by
TestUpgradeGuidanceKeepsCrossTargetAnswerForHomebrew, whose comment
predicted exactly this reading — "it reads like an ordering bug until you
see which question is being answered". Homebrew is a property of the
binary on THIS machine; when the check was asked about a different
target the answer belongs to that other machine, and `brew upgrade zero`
would change this one instead. The reasoning is now recorded at the
branch as well as in the test, so the next reader finds it in both
places.
The install docs claimed detection was the Cellar path alone, which
undersold it, and stated the self-update rule absolutely in one paragraph
before excepting mise in the next. Both say the same thing now.
* test(update): give the Homebrew fixtures a receipt, and stop gating the receipt rule on the host
The previous commit made a receipt the thing that distinguishes a keg from any
directory sitting under a path containing "Cellar". Two fixtures still built a
keg without one, so detection correctly answered standalone and both failed:
TestDetectInstallMethodRecognisesAHomebrewKeg keg path: got "standalone", want "homebrew"
TestApplyRefusesToUpdateAHomebrewKeg refusal does not name the command that works:
download release archive: dial tcp: lookup example.test
The second is worth reading twice: with detection returning standalone, Apply
stopped refusing and went to the network, so the failure surfaced as a DNS error
rather than as a missing refusal. Both fixtures write a receipt now.
The reason this reached CI at all is the second half. Every test of the receipt
rule passed runtime.GOOS to isHomebrewPath, which already takes the target as a
parameter, so all of them skipped on Windows and the rule could only ever break
somewhere else. They pass an explicit target now and run everywhere. The two
that exercise DetectInstallMethod stay host-gated, because those are about the
wired entry point rather than the rule, but the rule underneath them is no
longer invisible here.
Verified the receipt path arithmetic on both non-Windows targets rather than
trusting the host-gated tests: isHomebrewPath is false before the receipt is
written and true after, with the receipt at the keg directory hasHomebrewReceipt
actually reads.
10 KiB
Installing Zero
Pick whichever fits how you already install things. None of these is the blessed one.
| Method | Command | Self-update |
|---|---|---|
| Install script (Linux, macOS) | curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh | bash |
zero upgrade |
| Install script (Windows) | irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | iex |
zero upgrade |
| npm | npm install -g @gitlawb/zero |
zero upgrade (runs npm) |
| Release archive | download from Releases | zero upgrade |
| mise | mise use -g ubi:Gitlawb/zero |
mise upgrade |
go install |
go install github.com/Gitlawb/zero/cmd/zero@latest |
rerun the command |
| Source | go build -o zero ./cmd/zero |
rebuild |
zero upgrade follows whatever owns the binary, with one exception it cannot
detect. It delegates to npm for an npm install, refuses outright for a Homebrew
keg and names brew upgrade zero, and replaces the binary in place for
everything else — including a mise-managed one, which is the exception.
mise is the one case it cannot detect: a mise-managed binary is an ordinary
standalone install on disk, so zero upgrade will replace it and leave mise
describing a version that is no longer there. Use mise upgrade instead.
Release archives are the substrate for most of the above: the install scripts and the npm fallback both download a platform archive from a published GitHub Release and verify its checksum.
npm
npm install -g @gitlawb/zero
zero
The package supports Linux and macOS on x64 and arm64, and Windows on x64
(Windows on ARM runs the x64 build under emulation). It installs a small
zero wrapper plus, as an optional dependency, a platform payload with the
native binary and the bundled browser/terminal control helpers. There are no
install scripts and nothing is downloaded from outside the npm registry, so
the install is silent and works identically under npm, Bun, pnpm, and yarn —
no trust or approval steps. See NPM_PACKAGING.md for the
package architecture.
Requirements:
- Node.js 18+
- network access to npm
If the install skipped optional dependencies (npm install --omit=optional,
or a package manager configured to do so), the wrapper falls back to
downloading the binary from the matching GitHub Release, with checksum
verification. The fetch is retried on each run until a binary is in place (a
transient network failure heals itself; an install directory the current user
cannot write to keeps failing — rerun the install or the command below with
sufficient permissions). To trigger the fetch manually:
node "$(npm root -g)/@gitlawb/zero/scripts/postinstall.mjs"
Linux And macOS Script
Install the latest release:
curl -fsSL https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.sh | bash
From a checkout:
scripts/install.sh
Install a specific version:
ZERO_VERSION=0.1.0 scripts/install.sh
scripts/install.sh --version 0.1.0
Install somewhere else:
ZERO_INSTALL_DIR="$HOME/bin" scripts/install.sh
scripts/install.sh --install-dir "$HOME/bin"
Defaults:
- Repository:
Gitlawb/zero - Version: latest GitHub release
- Install path:
~/.local/bin/zero
Requirements: Bash, curl or wget, tar, and shasum or sha256sum.
Windows PowerShell Script
Install the latest release:
irm https://raw.githubusercontent.com/Gitlawb/zero/main/scripts/install.ps1 | iex
From a checkout:
powershell -ExecutionPolicy Bypass -File scripts/install.ps1
Install a specific version:
powershell -ExecutionPolicy Bypass -File scripts/install.ps1 -Version 0.1.0
Install somewhere else:
powershell -ExecutionPolicy Bypass -File scripts/install.ps1 -InstallDir "$env:USERPROFILE\bin"
Defaults:
- Repository:
Gitlawb/zero - Version: latest GitHub release
- Install path:
%LOCALAPPDATA%\zero\bin\zero.exe
mise
mise installs Zero straight from the GitHub Release
archives through its ubi backend, so there is nothing extra to publish and no
registry in the middle:
mise use -g ubi:Gitlawb/zero
Pin a version the same way you would any other tool:
mise use -g ubi:Gitlawb/[email protected]
Updates come from mise upgrade. zero upgrade also works, because a
mise-managed binary is an ordinary standalone install, but then mise's records
describe a version that is no longer on disk. Prefer mise upgrade.
go install
go install github.com/Gitlawb/zero/cmd/zero@latest
This builds from source, so it needs Go 1.26.6+ and it does not go through the release archives. Two consequences worth knowing before you pick it:
- On Linux you also need the sandbox helper, which is a separate binary and is not installed by this command. See Sandbox Helpers For Source Builds. Without it, native sandboxing is unavailable.
zero upgradetreats the result as a standalone install and will replace the binary with a release build rather than rebuilding from source. If you chosego installdeliberately, rerun it instead.
macOS and Windows need no extra helper.
From Source
git clone https://github.com/Gitlawb/zero.git
cd zero
go run ./cmd/zero
Build a local binary:
go build -o zero ./cmd/zero
Source builds require Go 1.26.6+.
Sandbox Helpers For Source Builds
Release archives include the platform sandbox helpers. If you build directly from source, build the helpers you need:
Linux:
go build -o zero ./cmd/zero
go build -o zero-linux-sandbox ./cmd/zero-linux-sandbox
go build -o zero-seccomp ./cmd/zero-seccomp
Put zero and zero-linux-sandbox in the same directory on PATH, for example
~/.local/bin. zero-seccomp is kept as a compatibility wrapper; the sandbox
helper applies the Unix-socket filter itself when that sandbox option is enabled.
Linux native sandboxing also requires Bubblewrap to be installed.
macOS uses the system sandbox and does not need an extra helper binary.
Termux (Android)
Zero can run natively on Android via Termux. Build with
GOOS=android to avoid the faccessat2 syscall that is blocked by Samsung's
seccomp filter on Android:
# Install Go in Termux
pkg install golang
# Build Zero for Android
git clone https://github.com/Gitlawb/zero.git
cd zero
CGO_ENABLED=0 GOOS=android GOARCH=arm64 go build -ldflags="-s -w" -o zero ./cmd/zero
# Move into PATH
mv zero ~/.local/bin/
Why
GOOS=android? Go 1.26+ detectsruntime.GOOS == "android"and skips thefaccessat2syscall insideos/exec.findExecutable, falling back to permission-bit checks. Without this flag, Android's seccomp sends SIGSYS and kills the process whenever Zero looks up a binary onPATH(git, sh, etc.).
DNS. Android does not expose /etc/resolv.conf. Go's pure-Go DNS resolver
needs one. Use proot to bind-mount Termux's resolver config:
pkg install proot
proot -b "$PREFIX/etc/resolv.conf:/etc/resolv.conf" zero
Create a wrapper at ~/.local/bin/zero to avoid typing proot every time:
#!/data/data/com.termux/files/usr/bin/bash
exec proot -b "$PREFIX/etc/resolv.conf:/etc/resolv.conf" ~/.local/bin/zero.bin "$@"
Scroll. On native Termux (not under PRoot), mouse scrolling works out of the
box. The TUI uses Bubble Tea's AllMotion mouse mode by default. If you run Zero
inside PRoot (e.g. through proot-distro), the scroll fix activates CellMotion
to avoid PRoot's ptrace interference with the 1003 escape sequence.
Providers. Zero works with any OpenAI-compatible provider on Termux. For example, to use OpenCode Zen's free tier:
zero providers add opencode \
--name opencode \
--model deepseek-v4-flash-free \
--base-url https://opencode.ai/zen/v1 \
--set-active
Windows source builds can use the main zero.exe as the command runner and setup
helper through Zero's built-in self-dispatch path. If you want a release-style
layout anyway, build the standalone helper executables next to zero.exe:
go build -o zero.exe ./cmd/zero
go build -o zero-windows-command-runner.exe ./cmd/zero-windows-command-runner
go build -o zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup
Release Archive Format
Release archives are named:
zero-v<version>-linux-<arch>.tar.gzzero-v<version>-macos-<arch>.tar.gzzero-v<version>-windows-<arch>.zip
Supported targets:
linux-x64linux-arm64macos-x64macos-arm64windows-x64(Windows on ARM runs this build under emulation)
Each archive must have a matching .sha256 file. The install scripts download
both files, verify the checksum, and then copy the binary into the install
directory.
Updating
Check for a newer release, then download, verify, and install it in place:
zero update --check
zero upgrade
See the update guide for update modes, flags, and platform details.
zero upgrade behaves differently depending on how Zero was installed, because
overwriting a binary a package manager owns leaves that manager describing a
version that is no longer there:
- npm: runs
npm install -g @gitlawb/zero@latestfor you. - Homebrew: refuses, and tells you to run
brew upgrade zero. Replacing the keg binary directly would be reverted by the nextbrew upgradeorbrew reinstall. - everything else: downloads the verified release archive and replaces the binary in place.
Homebrew is detected by the binary living inside a Cellar keg that carries
Homebrew's own INSTALL_RECEIPT.json. The keg shape alone is not enough: a
directory tree you built yourself can have exactly that shape, and refusing to
update an install Homebrew never touched would be a real cost for a made-up
reason. The receipt is the thing only Homebrew writes.
An ordinary install under /usr/local/bin is left alone even on an Intel Mac
where that is also the Homebrew prefix.