hams apply
Make your machine match what your Hamsfile says. This is the workhorse command — whether you’re bootstrapping a new machine or you just hand-edited a Hamsfile and want the change to land, apply is what does it.
In three steps: read your Hamsfiles → diff against the current machine state → install, remove, or update whatever’s different.
Usage
hams apply [flags]No flags? It applies everything in the current store to this machine.
Examples
# The everyday case: apply everything in the current store
hams apply
# Fresh machine: clone the store from GitHub and apply in one go.
# Add --bootstrap if the machine doesn't have Homebrew installed yet —
# hams will run the official install.sh for you (asks first on a TTY).
hams apply --bootstrap --from-repo=zthxxx/hams-store
# Not sure what's about to happen? Dry-run first
hams apply --dry-run
# Only interested in Homebrew and pnpm right now
hams apply --only=homebrew,pnpm
# Run everything *except* Ansible (which tends to be the slow one)
hams apply --except=ansibleFlags
| Flag | Type | What it does |
|---|---|---|
--from-repo | string | Clone the store from a remote GitHub repo and apply. Perfect for a new machine |
--only | string | Comma-separated providers to run. Everything else is skipped |
--except | string | Comma-separated providers to skip. Everything else runs |
--no-refresh | bool | Skip the refresh phase. hams normally probes the environment first to catch drift; --no-refresh trusts whatever’s in .state/ and skips straight to the diff. Useful when you just ran hams refresh and want to re-apply without re-probing |
--prune-orphans | bool | Process providers that have a state file but no hamsfile by removing every state-tracked resource. Destructive; default off. Useful when you’ve stopped tracking a tool and want hams to clean up. Be sure your hamsfiles are present (not in a half-pulled worktree) before using this — on a partial checkout this would mass-uninstall |
--bootstrap | bool | Auto-run a missing provider’s bootstrap script (e.g. Homebrew’s install.sh). Runs remote bash from the internet — opt in explicitly. Default off; interactive shells get a [y/N/s] prompt instead |
--no-bootstrap | bool | Skip the interactive bootstrap prompt that would otherwise appear on a TTY. Useful in CI when you want fail-fast behavior and don’t want hams to ever auto-install anything |
--dry-run | bool | Print the plan, don’t execute it |
What actually happens
Under the hood, an apply goes through these stages:
- Acquire the lock. A global lock prevents two apply runs from stepping on each other
- Refresh. For each provider, scan the machine and write the results to
.state/ - Compute the diff. Compare the Hamsfile (desired) with
.state/(actual). The result is the list of things to install, remove, or change - Execute in priority order. Providers run in the order defined by
provider_priority. By defaultbashgoes first, which lets you bootstrap dependencies like Homebrew inside a bash step - Write state back. Each resource’s
.state/<machine-id>/<Provider>.state.yamlentry updates as soon as that resource finishes
Ctrl+C in the middle? Fine. The next apply picks up where this one left off — finished resources aren’t repeated.
About the bootstrap prompt
On a fresh machine, a provider’s prerequisite may be missing. Several providers now opt into the bootstrap consent flow — each with its own install script declared in the provider’s manifest:
| Provider | Install script | Host |
|---|---|---|
brew (Homebrew) | curl -fsSL .../install.sh | macOS / Linuxbrew |
pnpm | npm install -g pnpm | requires npm already on PATH |
duti | brew install duti | macOS; chains through brew |
mas | brew install mas | macOS; chains through brew |
ansible | pipx install --include-deps ansible | requires pipx (apt install pipx / brew install pipx) |
Other providers (npm, cargo, goinstall, uv, code, apt, defaults, git) intentionally
DO NOT adopt --bootstrap — language runtime installation is user-owned (nvm / fnm / rustup /
distro), code needs a GUI app, apt/defaults are platform-gated, git is pre-present
on any machine that ran hams’s installer. For these, hams emits a plain “not found in PATH” error
with the install hint.
When hams reaches a provider whose prerequisite is absent, it stops and does one of three things:
- If you passed
--bootstrap, it runs the provider’s declared install script through the bash provider and retries. - If stdin is a terminal and you didn’t pass
--no-bootstrap, it shows the exact script about to run, warns about expected side effects (sudo, Xcode CLT on macOS, corporate proxies), and asks[y/N/s](yes / no / skip-this-provider). - Otherwise (CI, pipe,
--no-bootstrap), hams surfaces an actionable error and exits — no network traffic, no side effects. The error includes the binary name, the exact script, and the--bootstrapremedy so you can copy-paste.
hams NEVER auto-executes a remote install script without an explicit opt-in. This is the same
pattern as --prune-orphans: destructive or remote-executing defaults are always behind a flag.
The brew → {duti, mas} chain resolves in one hams apply --bootstrap run because the DAG
orders brew first; once brew is installed, brew install duti works seamlessly.
On macOS, Homebrew’s install.sh may trigger the Xcode Command Line Tools GUI installer
which blocks stdin. If your terminal appears to hang for minutes, switch to your desktop
and look for a modal dialog waiting behind your IDE.
About sudo
If your profile includes a provider that needs elevated privileges (apt being the usual example),
hams asks for your sudo password once at the start, then quietly keeps the ticket alive every 4
minutes so you’re never interrupted mid-apply.
When something fails
A single failure doesn’t tank the whole run. hams keeps going with the rest of the resources,
then reports what failed at the end and exits with code 4 (partial failure). Full logs sit in
~/.local/share/hams/, organised by month. Fix the cause and apply again — only the failed
resources will retry.
On a store you don’t fully trust yet, always --dry-run first. Look at the plan, then run it
for real.