Skip to main content

Maintaining the setup

Everything below is "edit a managed file, chezmoi apply, push." Pull on other machines with chezmoi update.

Add a shell helper

$EDITOR ~/.oh-my-zsh/custom/my-helper.zsh # one function per file; OMZ auto-loads
chezmoi add ~/.oh-my-zsh/custom/my-helper.zsh
chezmoi cd && git add -A && git commit -m "Add my-helper" && git push && exit

Add a secret

vault kv put secret/users/<you>/myservice MY_TOKEN=
vault-agent restart && exec zsh # auto-discovered, no template edits

Add a tool

chezmoi edit ~/.Brewfile # (or ~/.config/dotfiles/apt-packages.txt)
chezmoi apply

Add an SSH host

~/.ssh/config is generated from the ssh: block in .chezmoidata.yaml — don't hand-edit the rendered file, it gets overwritten on the next apply.

A node behind the dagda bastion is one line; it inherits the shared ProxyJump / IdentityFile / StrictHostKeyChecking set:

ssh:
jump:
hosts:
- "192.168.100.221" # ← new node, nothing else to write

Anything else is a {patterns, options} pair, where options is any set of ssh keywords (a list value repeats the keyword, e.g. two IdentityFile lines):

ssh:
hosts:
- patterns: ["buildbox", "buildbox.stump.rocks"]
options:
HostName: "10.0.0.9"
User: "joestump"

Then chezmoi apply && ssh -G <host> to confirm what ssh actually resolves.

Order is load-bearing: ssh is first-match-wins per keyword, and the template emits ssh.hostsssh.jump.hostsssh.multiplex. So an entry in ssh.hosts always beats the shared groups below it — that's how you special-case one node without splitting a group apart. Quote your values ("yes", "443"): bare yes is a YAML boolean and would render as true, which ssh rejects.

Per-machine overrides go in that box's ~/.config/chezmoi/chezmoi.toml under [data.ssh], same as [data.claude]. Maps deep-merge, lists replace — so this repoints the bastion on one box while keeping the shared IdentityFile / IdentitiesOnly / StrictHostKeyChecking:

[data.ssh.jump.options]
ProxyJump = "root@other.bastion"

whereas setting [data.ssh.jump] hosts = [...] replaces the host list outright rather than appending to it.

Add a Claude MCP server or plugin

Edit ~/.config/dotfiles/mcp-servers.json (+ its OpenBao secret) or claude-plugins.tsv, then chezmoi apply. Full reference: MCP servers.

chezmoi apply installs everything, but the device link is interactive:

signal-link # scan the QR from Signal → Linked Devices

Details (daemon control, troubleshooting): Signal.

Add a prompt glyph

Edit PROMPT_GLYPHS in ~/.zshrc. Awkward to type? Use the codepoint: $'' is a heart.

Search ~/src with qmd

If ~/src exists, apply indexes each top-level repo into its own qmd collection (named after the directory) for local hybrid markdown search — so agents can qmd query -c <repo> per project. The logic lives in one place, ~/.config/dotfiles/qmd-index-src.zsh, and is driven three ways:

dot # → "🔎 Re-index ~/src (qmd)" (re-run any time)
status # → the 🔎 qmd row: collection count, doc total, embed state
chezmoi apply # runs the same indexer (a run_onchange_after_ hook)

It also re-indexes on its own, daily (launchd on macOS, a systemd --user timer on Linux — the same mechanism as scheduled czu), so markdown that lands in ~/src without a chezmoi apply still gets picked up. The scheduled run logs to ~/.cache/qmd-index-src.log and, being idempotent, is safe to overlap with a manual re-index.

Dirs with no markdown are skipped (no empty collections), and re-indexing is idempotent — existing collections are refreshed incrementally, not recreated. Only the BM25 (keyword) index is built automatically; the ~2GB embedding models are opt-in (exactly like the qmd install), so semantic search needs a manual qmd embed — that's the "embed pending" note the status panel shows.

Update everything

💡 One step: czu — brings this machine fully current in a single command: it runs chezmoi update (git pull + apply), then vault-agent restart (re-render secrets from OpenBao now instead of waiting ~5 min), then exec zsh to reload the shell so new config + secrets take effect. Extra args pass straight through, so czu --refresh-externals also re-pulls themes, plugins, marketplaces, and the signal-mcp clone.

Installing a fresh spoke instead? czinit <host> does the whole thing over SSH — see Install a Spoke.

czu also runs on its own, every 6 hours (launchd on macOS, a systemd --user timer on Linux) — every box stays current without you typing anything. It stays silent on success; a failed scheduled run sends a Signal note-to-self once (not every retry), and another once it recovers.

czu bringing a machine fully up to date, with per-phase checked sections

Under the hood, or to run the pieces by hand:

chezmoi update # git pull + apply (re-runs changed run_onchange_ scripts)
chezmoi apply --refresh-externals # also re-pull themes/plugins/marketplaces + signal-mcp
brew bundle --global # macOS tools

Claude plugins update themselves — the run_after_ script reinstalls the local-path marketplace and updates remotes on every apply. There's no bulk claude plugin update; to force one by hand it's claude plugin update <plugin>@<marketplace> (e.g. qmd@qmd). See Plugins.

CI

Every push runs Gitea Actions (.gitea/workflows/ci.yml):

  • bats — the BATS test suite.
  • lint — shellcheck (incl. rendered *.tmpl), zsh -n, JSON, TOML, YAML, and a gitleaks secret scan.

Run the tests locally with bats test/. This docs site ships from a separate pages workflow to Garage Pages.