One YAML profile. Every binary. Same result on every machine.
binstaller is a native CLI that installs binary tool distributions from a single declarative profile β
with a dry-run plan you read first, SHA-256 verification you can trust, and a lock file that pins the result.
π Website Β Β·Β π Wiki Β Β·Β π¦ Releases Β Β·Β π Security model
curl --proto '=https' --tlsv1.2 -sSfL \
https://github.com/worxbend/binstaller/releases/latest/download/install.sh | shThe script downloads the release tarball, verifies its SHA-256 checksum, additionally verifies the
keyless Sigstore signature when cosign is installed, and installs binstaller to ~/.local/bin.
| Knob | Effect |
|---|---|
BINSTALLER_INSTALL_DIR |
π Install somewhere other than ~/.local/bin. |
BINSTALLER_VERSION |
π Pin a specific version, e.g. v0.2.0. |
BINSTALLER_UPDATE_PATH=1 |
π§ Append the install dir to ~/.bashrc and ~/.zshrc. |
Note
By default nothing in your shell configuration is modified β the script just prints the
export PATH=... line to run.
π¦ Release artifacts β every .tar.gz and the example config ship with a .sha256 file
binstaller-<version>-linux-amd64.tar.gzbinstaller-<version>-linux-arm64.tar.gzbinstaller-<version>-macos-amd64.tar.gzbinstaller-<version>-macos-arm64.tar.gzconfig.example.<version>.yamlinstall.sh
1οΈβ£ Copy a profile
cp config.example.yaml config.yaml2οΈβ£ Read the plan β nothing is written
binstaller plan3οΈβ£ Apply it
binstaller apply4οΈβ£ Watch for drift
binstaller versionsNarrow the blast radius β both flags are repeatable, --only is applied first and --skip second:
binstaller plan --only yazi
binstaller apply --skip neovimPin everything to a lock file:
binstaller lock --lock-file binstaller.lock.json
binstaller apply --locked --lock-file binstaller.lock.json| π Plan before apply Every version, URL, archive mapping and symlink resolved and printed. Zero filesystem writes. |
π Checksums, loudly SHA-256 verified when configured β and a missing checksum is called out in the plan. |
π§ Lock fileslock writes resolved versions and digests; apply --locked refuses to drift. |
| π‘οΈ SSRF-guarded HTTPS only. Loopback, private and cloud-metadata hosts rejected β on every redirect hop. |
β©οΈ Resumable state State is saved after each tool and keyed to the manifest fingerprint, so re-runs skip what worked. |
β‘ Native, no JVM GraalVM native images for Linux and macOS, amd64 and arm64. |
| π¦ Archives handled Direct binaries, zip, tar.gz, tar.xz β member paths validated so nothing escapes staging. |
π Sudo is opt-in System-wide symlinks need allowSudoSymlinks and are flagged sudo risk in the plan. |
π§± Strict modepolicy.mode: strict rejects latest-URLs, missing checksums, sudo symlinks and tar.xz fallbacks. |
Important
The scope is deliberately narrow. binstaller is not a package manager, a dotfiles runner,
an installer-script host, a shell-command runner, or a multi-OS provisioner. Manifest installer:
blocks are rejected at load time, on purpose.
| Command | Purpose | Writes files |
|---|---|---|
π plan |
Render the resolved install plan. | β |
π apply |
Download, verify, stage, install, symlink, save state. | β |
π versions |
Print package versions and available GitHub release updates. | β |
π§ lock |
Resolve and write a JSON lock file. | π§ lock file only |
βοΈ Shared options and exit codes
Shared options
| Flag | Meaning |
|---|---|
--config FILE |
Path to the YAML profile. Defaults to config.yaml in the current directory. |
--state FILE |
Override the profile state file for apply. |
--reset-state |
Ignore saved execution state and start fresh. |
--verbose |
Show additional command diagnostics. |
--only TOOL |
Include only a named tool (plan, apply, versions, lock). Repeatable. |
--skip TOOL |
Omit a named tool (plan, apply, versions, lock). Repeatable. |
plan and apply also accept
| Flag | Meaning |
|---|---|
--locked |
Require a compatible JSON lock file before running. |
--lock-file FILE |
Path to the JSON lock file used by --locked. |
apply also accepts
| Flag | Meaning |
|---|---|
--parallelism N |
Number of tools downloaded and staged concurrently. Default 4. Must be at least 1. |
lock also accepts
| Flag | Meaning |
|---|---|
--lock-file FILE |
Lock file to write. Default binstaller.lock.json. |
Exit codes
| Code | Meaning |
|---|---|
0 |
β Completed successfully, including help and plan. |
1 |
β Manifest loading/resolution failed, selection invalid, apply failed, or state persistence failed. |
2 |
One file describes the whole toolchain β reviewable in a pull request, validated before a byte is downloaded.
apiVersion: binstaller.io/v1alpha1
kind: BinaryDistributionProfile
spec:
policy:
mode: strict # π§± reject latest-URLs, missing checksums, sudo symlinks
appsDir: "${HOME}/.apps"
allowSudoSymlinks: false
versions:
lazygit: 0.61.0 # π pinned
kubectl:
resolver: # π resolved at plan time
type: http-text
url: https://dl.k8s.io/release/stable.txt
plan:
- name: lazygit
kind: binary-tool
spec:
versionRef: lazygit
installDir: "${appsDir}/lazygit"
download:
url: "https://github.com/jesseduffield/lazygit/releases/download/v${version}/lazygit_${version}_Linux_x86_64.tar.gz"
filename: lazygit.tar.gz
checksum: # π verified before anything is unpacked
algorithm: sha256
value: 45d49e06β¦78633d
archive:
type: tar.gz
extract:
files:
- from: lazygit
to: bin/lazygit
executables:
- path: bin/lazygitπ Full field reference: docs/manifest-reference.md Β·
π Complete working profile: config.example.yaml
apply writes state after each per-tool result. State is tied to the profile name and the manifest
fingerprint, so a later apply can skip tools already completed for the same profile.
- π The state path comes from
--stateorspec.policy.stateFile. - π State paths are current-directory filenames only β absolute, nested and empty paths are rejected.
- π§
plannever touches state. - π Use
--reset-stateto intentionally ignore compatible saved state and retry from the beginning.
Requirements: JDK 21+ Β· GraalVM 21 only for local native images.
./mill __.compile # ποΈ compile everything
./mill __.test # π§ͺ run every test module
./mill mill.scalalib.scalafmt/checkFormatAll # π¨ formatting gate
./mill app.run plan --config config.example.yaml # βΆοΈ run from sourceBuild a native image locally:
GRAALVM_HOME=/path/to/graalvm ./mill app.nativeImageπ§© Module graph
app βββΆ cli βββΆ core βββΆ config
| Module | Responsibility |
|---|---|
config |
YAML reading, typed decoding, validation, unsupported-field rejection. |
core |
Resolution, downloads, checksums, extraction, staging, symlinks, state, events. |
cli |
Picocli parsing, exit codes, colored progress, script-friendly output. |
app |
Process entry and exit-code propagation only. |
| Doc | What's inside |
|---|---|
| π Website | Feature tour, screenshots and quick start. |
| π Wiki | Getting started, recipes, troubleshooting, FAQ. |
| ποΈ Architecture | Module graph, data flow, event contract. |
| π§© Developer API | Typed plan and lock API, lifecycle, injection. |
| π Manifest reference | Profile shape, policy, versions, downloads, archives, symlinks. |
| π Security model | Trust boundaries, checksums, archive safety, sudo policy, known risks. |
| π§ͺ Testing guide | Project-native checks and test patterns. |
| π’ Release guide | Native artifacts, release workflow, smoke checks. |
Found it useful? β Star the repo β it genuinely helps.
π Report a bug Β Β·Β π‘ Request a feature Β Β·Β π Website
Built with Scala 3, Mill and GraalVM.