Skip to content

Spike: evaluate chezmoi as an alternative to stow + per-host overlays #4

@rdlu

Description

@rdlu

Sibling to #3. Option D in that issue is "migrate to chezmoi" — this issue is the deeper exploration so we can decide between Option A (stow + per-host overlay) and Option D before committing.

Prior attempt didn't stick because the model wasn't fully clicking. This issue captures the things that probably tripped me up, what chezmoi would actually look like for this repo, and a concrete spike plan to try it in a sandbox without ripping out stow.

The mental model shift (this is the part that's easy to miss)

Stow's model: the repo is the live config; stow makes ~/.config/foo a symlink into the repo. Edit a file → repo changes immediately.

Chezmoi's model: the repo is a source state. chezmoi apply renders the source into the target (~/.config/foo) by copying (default), not symlinking. So:

  • Editing ~/.config/foo directly does not change the repo. You either chezmoi edit (which edits the source and re-applies), or edit the target then chezmoi re-add to pull changes back into the source.
  • This decoupling is what enables templating, conditionals, and secrets — but it's the workflow shock that probably bit me last time.
  • You can configure chezmoi to symlink instead of copy on a per-file basis (symlink_ prefix), which gets you closer to stow's behavior, but you lose templating on those files.

File naming conventions (the second thing that's confusing)

Source filenames encode metadata via prefixes:

Prefix Meaning
dot_foo renders as .foo
private_foo mode 0600
executable_foo mode 0755
run_once_install.sh run once, ever (tracked by hash)
run_onchange_install.sh run when the script's contents change
*.tmpl rendered as a Go template before being copied
encrypted_foo decrypted on apply (age/gpg/1Password)
symlink_foo target is a symlink, not a copy

So a fish ssh wrapper would live at dot_config/fish/conf.d/ssh.fish, and the niri solaar service would be dot_config/systemd/user/solaar.service. The dot_ rename feels weird but is just because raw .foo files don't show up well in editors and git tooling.

Templating — the killer feature for per-host

For our XPS divergence, the entire hosts/xps/ overlay collapses into templates:

```gotmpl

dot_config/waybar/config.jsonc.tmpl

"temperature": {
{{ if eq .chezmoi.hostname "xps" -}}
"thermal-zone": 8,
"critical-threshold": 90,
{{- else -}}
"critical-threshold": 80,
{{- end }}
"format": " {temperatureC}°C"
}
```

And XPS-only .desktop files become *.tmpl files that emit empty output (skipped) on other hosts, or live behind {{- if eq .chezmoi.hostname \"xps\" -}} guards. No separate overlay directory.

run_onchange_* scripts replace most of the justfile install recipes — chezmoi runs them automatically when their content changes (e.g., a run_onchange_install-packages.sh.tmpl that emits a pacman -S --needed <list> command and runs only when the list changes).

What this would look like for our repo

Today After chezmoi
stow.sh, three stow recipes chezmoi apply
pc/xps branch {{ if eq .chezmoi.hostname \"xps\" }} template guards
setup/*.fish, justfile install recipes run_once_* and run_onchange_* scripts
Manual systemctl --user enable after stow run_onchange_enable-services.sh.tmpl
gnupg/ (WSL-only) delete (WSL is gone)
home/dot-local/share/applications/mimeinfo.cache not tracked (was a stow artifact)

What the justfile keeps doing: package install (Chaotic AUR keyring, paru bootstrap, distro-level setup that has to run before chezmoi exists). Day-2 ops (update, doctor) live there too. So this isn't "replace just" — it's "replace stow + branch overlays".

The spike plan (try it without commitment)

  1. Install chezmoi, point it at a throwaway target dir: chezmoi init --source ~/dotfiles-chezmoi-spike --destination ~/chezmoi-test. No risk to the real ~/.config.
  2. Migrate one config: pick terminal/dot-tmux.conf (small, single file, has the F12 host-agnostic block). chezmoi add ~/.tmux.conf, see how it lands in the source.
  3. Add a template: pick waybar's config.jsonc, port the XPS thermal override as a {{ if eq .chezmoi.hostname \"xps\" }} block. Run chezmoi apply --dry-run -v to see the rendered output for both hostnames (override --init data to fake the hostname).
  4. Try a run_onchange script: convert one justfile recipe (e.g. wpaper-reload) into a chezmoi script and confirm it only re-runs when its content changes.
  5. Decide: at this point the workflow shock is the main remaining unknown. Use the spike repo for ~1 week as the source of truth for one config that changes a lot (probably nvim or fish). If chezmoi edit + chezmoi apply feels ergonomic enough, commit to migration. If not, fall back to Option A in Reconcile main and pc/xps: pick a per-host strategy #3.

Common gotchas (worth pre-loading)

  • chezmoi apply is the verb you'll forget. Edits to the source aren't live until you apply. Add a fish abbr or autocmd.
  • chezmoi edit -a edits source and applies in one shot — this is closer to the stow muscle memory. Use it.
  • chezmoi diff before apply is your safety net. Make it a habit.
  • chezmoi cd drops you into the source repo for git operations. Don't cd there manually and forget — chezmoi cd ensures you're in the right place.
  • Templates are Go templates, not Jinja/Handlebars. The {{- / -}} whitespace control trims newlines. Leaving them off creates blank lines in rendered output — usually harmless, occasionally breaks parsers.
  • .chezmoiignore controls what isn't applied per-host. Equally powerful as .tmpl guards, sometimes simpler.
  • Symlink mode opt-in: if you want a file to behave like stow (symlink, edit-in-place), name it symlink_foo. Don't fight the default copy mode for files where you actually want symlink behavior.

Open questions

  • Workflow shock (copy vs symlink) — willing to live with chezmoi edit -a as the new default, or is the stow "just edit the file" workflow load-bearing?
  • Secrets: the gnupg/ dir is WSL-only and getting deleted, but is there other host-specific stuff that should be encrypted (ssh config snippets, work-only credentials, API tokens in fish env)? Chezmoi has age/gpg/1Password integration — worth using if so.
  • Migration appetite: big-bang (one PR replaces stow), or coexist (chezmoi handles new configs, stow keeps managing existing ones until they're ported)?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions