Skip to Content
DocumentationDocumentationCLI Referencehams apply

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=ansible

Flags

FlagTypeWhat it does
--from-repostringClone the store from a remote GitHub repo and apply. Perfect for a new machine
--onlystringComma-separated providers to run. Everything else is skipped
--exceptstringComma-separated providers to skip. Everything else runs
--no-refreshboolSkip 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-orphansboolProcess 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
--bootstrapboolAuto-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-bootstrapboolSkip 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-runboolPrint the plan, don’t execute it

What actually happens

Under the hood, an apply goes through these stages:

  1. Acquire the lock. A global lock prevents two apply runs from stepping on each other
  2. Refresh. For each provider, scan the machine and write the results to .state/
  3. Compute the diff. Compare the Hamsfile (desired) with .state/ (actual). The result is the list of things to install, remove, or change
  4. Execute in priority order. Providers run in the order defined by provider_priority. By default bash goes first, which lets you bootstrap dependencies like Homebrew inside a bash step
  5. Write state back. Each resource’s .state/<machine-id>/<Provider>.state.yaml entry 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:

ProviderInstall scriptHost
brew (Homebrew)curl -fsSL .../install.shmacOS / Linuxbrew
pnpmnpm install -g pnpmrequires npm already on PATH
dutibrew install dutimacOS; chains through brew
masbrew install masmacOS; chains through brew
ansiblepipx install --include-deps ansiblerequires 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:

  1. If you passed --bootstrap, it runs the provider’s declared install script through the bash provider and retries.
  2. 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).
  3. 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 --bootstrap remedy 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.

Last updated on