Every Homie command except hm init and hm selfupdate expects to be
run from the root of a user environment repo, or with HM_REPO set to
its path.
hm init#
Interactively scaffolds a brand new user environment repo. Run this once
on the first machine. Subsequent machines clone the result via
bootstrap.sh.
hm init ~/dotfilesYou can also pass flags non-interactively (useful in CI):
hm init \
--name "Scout Homes" \
--email scout@homie.sh \
--github-user scouthomes \
--github-repo dotfiles \
--profile personal \
--shell zsh \
~/dotfileshm apply#
Full reconciliation pass — detect → load config → run pre-scripts →
install native packages → install declared backends (alphabetical order:
brew, flatpak, snap, …) → clone/update [externals] git repos →
materialize home/ (symlinks + rendered templates) → run scripts →
summary. Backend phases skip with a warning when the tool isn’t on PATH
or the backend name isn’t recognized. Externals run before the home
phase so templates and symlinks can point into a checkout that’s
guaranteed to exist; entries with a ref are held at it, unpinned
entries fast-forward to the remote default branch (see
Config).
hm applyapply is idempotent. Running it twice in a row should produce a clean
summary with nothing changed.
Flags:
--skip-packages— skip the native and backend package phases.--skip-externals— skip the externals (git clone/update) phase.--skip-scripts— skip pre-scripts and post-scripts.--no-tty— force plain output (no Bubble Tea spinner). Auto-detected when stdout isn’t a terminal; this flag overrides the detection.--verbose— raise log level to Debug.
hm home#
Just the home phase of apply. Walks home/ and any active sibling
home.tag-<X>[.tag-<Y>...]/ tree, then for each file:
- Plain files are symlinked into
$HOMEat the matching path. If a real file already lives at the destination, it’s backed up to<path>.homie-backup-<timestamp>before linking — Homie never silently overwrites your data. - Files ending in
.tmplare rendered through Gotext/template+ Sprig with the active data set and written into$HOMEwith the suffix stripped. Source mode is preserved (sohome/bin/foo.sh.tmplrenders executable).
When two trees claim the same target, the more-specific tree (more required tags in its directory name) wins; same-specificity collisions error out so you can disambiguate. See Dotfiles for the full model.
Flags:
--dry-run— write nothing. Prints the plan (every link and render target with its winning source), then the full rendered content of every active template. Use it to preview how templates resolve on this host before applying; exits non-zero if any template fails to render.
hm render#
Renders a single .tmpl file to stdout — no writes, no UI chrome, safe
to pipe. The data is exactly what a real hm home would use on this
host: active tags, [vars], user identity, distro, and the hasTag
helper, so the preview is faithful.
hm render home/.gitconfig.tmplThe path is tried as given first, then relative to the repo root, so
the command works from anywhere. A parse or execution error exits
non-zero — handy as a template check in CI, and it gives automated
agents a feedback loop for template authoring without touching your
real $HOME. To preview every active template at once, use
hm home --dry-run.
hm context#
Prints the exact data passed to every template render on this host, as
JSON. The keys match the template fields one-to-one — a key named
"Email" means a template may reference {{ .Email }} — so one read
tells you (or an agent) every field a template can use, with the values
it would resolve to right now.
$ hm context
{
"Name": "Scout Homes",
"Email": "scout@homie.sh",
"Profile": "personal",
"DefaultShell": "zsh",
"Distro": "fedora",
"Arch": "amd64",
"IsContainer": false,
"IsRoot": false,
"Tags": ["amd64", "coach", "fedora", "personal"],
"Vars": { "EDITOR": "nvim" }
}Output is always JSON, nothing else on stdout — safe to pipe into jq.
The hasTag helper and the Sprig function library are callable from
templates but aren’t data fields, so they don’t appear here. Pairs
naturally with the preview commands: introspect the context, then check
a template with hm render or hm home --dry-run. See
Dotfiles for the full data reference.
hm install#
Just the package phases. Resolves [packages].all + [packages].<distro>
plus matching [packages."tag:X"] against the detected package manager
and installs missing entries, then runs the non-native backend phases
([packages.brew], [packages.flatpak], [packages.snap]) in the same
order. On
unsupported distros, prints a friendly “not yet supported” notice and
skips the native phase. When a backend’s tool isn’t on PATH, prints a
warning and skips that backend.
hm run#
Just the script phase. Runs scripts/*.sh in lexical order, each as a
separate bash subprocess with HM_TAGS, HM_REPO, HM_HOME, and every
[vars] entry exported to its environment.
Scripts are user code — Homie doesn’t enforce idempotency. Convention is
that each script is individually idempotent (e.g. command -v X && exit 0 at the top).
Run from a terminal, scripts inherit your stdin/stdout/stderr, so an
in-band prompt (a sudo password, gh auth login, a package-manager
confirmation) reaches you directly. When stdin isn’t a terminal (CI,
piped, redirected), output is captured per-script and stdin is
/dev/null, so a script that would block on a prompt fails fast instead
of hanging.
Sibling directories named scripts.tag-<X>[.tag-<Y>...]/ run only when
all of their tags are active (AND), mirroring the home.tag-X/ trees.
Plain scripts/ always runs. Scripts are ordered by filename across
every active tree — the numeric prefix is the single global order, so a
scripts.tag-fedora/05-repos.sh runs at position 05 next to a
scripts/04-base.sh. The tag trees decide which scripts participate, not
a separate ordered stream. The same filename appearing in two active
trees is a hard error — unlike the home/ tree, scripts have no
“more-specific source wins” override rule, because an imperative script
has no single source of truth to override. See
Dotfiles for the parallel file-tree model.
Flags:
--phase=post(default) — every script whose name does NOT begin withpre-. The “scripts” step ofhm apply.--phase=pre— onlypre-*.shscripts. The “pre-scripts” step ofhm apply; useful for setting up third-party package sources beforehm install.--phase=all— pre-scripts then post-scripts, matching the orderhm applyuses.
hm status#
Read-only view of what apply would do. No changes, no installs, no
file writes. Useful for previewing, for CI checks, or for confidence
before a real run.
Flags:
--json— emit the same information as one JSON document: detected environment, repo path, identity, profile, active tags, the native and per-backend package lists, config warnings, and the error/warning counts from a doctor pass. When no environment repo is found,"repo"isnulland the command still exits zero; anHM_REPOthat points at a directory without ahomie.tomlis an error instead.
hm doctor#
Walks the repo and reports:
- Broken symlinks (target doesn’t exist).
- Symlinks in
$HOMEthat point outside the repo. - Missing packages.
- Unrendered templates.
- Scripts that aren’t executable, and filename collisions between active script trees.
- Tag-gated
home.tag-X/andscripts.tag-X/trees that aren’t active on this host, and which multi-tag[packages."tag:X.tag:Y"]blocks are active (informational). - Unknown distro detection.
Exit code is 0 if everything is clean, 1 otherwise — useful as a
post-deploy health check in CI.
Flags:
--json— emit the findings as structured records instead of the styled report:{ "findings": [ { "severity": "warn", "area": "packages", "message": "1 not installed: ripgrep" } ], "errors": 0, "warnings": 1 }Severity is
error,warn, orinfo; area is the check group (env,config,home,link,render,packages,scripts). The exit-code rule is unchanged — parse the document, then check the exit code. A non-zero exit caused by error findings still emits one valid JSON document on stdout; a failure before the checks run (no repo, unreadable config) prints to stderr with no JSON.
hm bootstrap#
Installs the minimum tools needed for apply to proceed: git and
ca-certificates. Called by bootstrap.sh on a fresh machine before
the user repo is cloned. Most users never invoke this directly.
hm selfupdate#
Updates the hm binary itself to the latest release. Resolves the
newest tag, downloads the binary for your OS and architecture, verifies
it against the release’s SHA256SUMS, and atomically replaces the
running binary — the same checks the install script performs.
hm self-update is an alias.
hm selfupdate # update in place
hm selfupdate --check # just report whether a newer release existsUpdating writes to the directory hm lives in: a binary in
/usr/local/bin usually needs sudo hm selfupdate, while the default
user install in ~/.local/bin needs no root. A build from source or a
Homebrew-managed binary refuses to self-update — rebuild it, or let the
tool that installed it do the upgrade.
Flags:
--check— report whether a newer release exists without installing it.
Help topics#
Beyond per-command --help, the binary ships one reference topic:
hm help templating— the template data fields, thehasTaghelper, Sprig availability, and the missing-key rules. The offline version of the Dotfiles reference;hm help templateandhm help templatesresolve to the same page.
Global flags#
These work on every subcommand:
--repo <path>— point at a user repo other than the cwd (or useHM_REPO=<path>).--no-tty— force plain output.--verbose— Debug-level logging.--version— print version and exit.
