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.hosts → ssh.jump.hosts → ssh.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.
Link Signal on a new node
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 runschezmoi update(git pull + apply), thenvault-agent restart(re-render secrets from OpenBao now instead of waiting ~5 min), thenexec zshto reload the shell so new config + secrets take effect. Extra args pass straight through, soczu --refresh-externalsalso re-pulls themes, plugins, marketplaces, and thesignal-mcpclone.Installing a fresh spoke instead?
czinit <host>does the whole thing over SSH — see Install a Spoke.
czualso 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.

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.