Migrating from GNU Stow to Chezmoi
Transitioning from GNU Stow to Chezmoi
I spent several years utilizing GNU Stow to handle my dotfiles—I even wrote a somewhat cheesy article about that specific configuration back in 2023. While Stow was a reliable tool, the overhead of managing symlinks across various machines eventually became a minor inconvenience a real headache.
After exploring alternatives and briefly contemplating building my own solution, a colleague suggested chezmoi. I've since adopted it and am quite pleased; it fulfills all my requirements and now even manages my agent skill files.
The Hardware Ecosystem
My current environment consists of three distinct macOS machines:
- MacBook Pro: Dedicated to professional work.
- MacBook Air: Used for personal projects.
- Mac Mini: Functions as a lightweight personal server.
Since the Mini still utilizes my shell, it shares the same dotfiles. I also maintain a few Linux VMs, though I rarely require my full dotfile suite on server environments.
The "Old Way": GNU Stow
Under the Stow model, configuration files lived in a git repository organized into "packages." Stowing a package created symlinks from the repo into the $HOME directory.
| Feature | GNU Stow Experience |
|---|---|
| Learning Curve | Extremely low; very intuitive. |
| Operation | Idempotent commands. |
| Editing | Direct write-through via symlinks. |
| Pain Point | Dirty git trees and merge conflicts. |
The "write-through" nature was the biggest flaw. I would often discover unplanned changes on my MacBook Air that conflicted with updates pushed from my Pro. Furthermore, bootstrapping a new Mac was a chore: I had to clone the repo, manually delete existing files (like ~/.zprofile), and remember the exact names of packages to restow. Homebrew and system settings were handled by separate, disconnected scripts.
Understanding the Chezmoi Workflow
Chezmoi operates differently. It maintains a source directory located at ~/.local/share/chezmoi, which is a standard git repository.
How files are handled:
- Running
chezmoi add ~/.zshrccopies the file into the source directory asdot_zshrc. - Adding a nested file like
~/.config/gh/config.ymlcreatesdot_config/gh/config.yml. - I rely on
chezmoi addto handle naming rather than creating them manually.
The source tree mirrors the home directory, but uses specific prefixes to encode attributes:
dot_: Represents a leading period (e.g.,.zshrc).private_: Restricts permissions to the owner (setting them to )..tmpl: Indicates a Go template, allowing for machine-specific data.
Unlike Stow, changes in the home directory do not automatically update the source repo. I use chezmoi diff to see discrepancies and chezmoi apply to synchronize them. This intentional separation is my favorite feature.
The Architecture
My Current File Tree
I prefer defaults over heavy customization, so my tree is relatively lean. I use chezmoi cd to enter the source directory:
~/.local/share/chezmoi
.chezmoi.toml.tmpl(Config template).chezmoiignore(Files to keep in source only).chezmoiscripts/macos/run_onchange_after_disable-macos-animations.shrun_onchange_after_init-macos-machine.sh.tmplrun_onchange_before_install-homebrew-bundle.sh.tmpl
.gitignoreBrewfileREADME.mddot_agents/skills/(go-modernize, go-styleguide, meatspeak)dot_claude/(settings.json, symlink_skills.tmpl)dot_codex/private_config.tomldot_config/gh/(config.yml, private_hosts.yml)ghostty/config
dot_gitconfigdot_gitconfig-persdot_gitconfig-werkdot_shellcheckrcdot_zsh_aliasesdot_zshrc
Advanced Configurations
Git Identity Routing
I organize my work under ~/canvas/werk/ and personal projects under ~/canvas/pers/. To handle different emails, I use a standard Git feature in my main .gitconfig:
[!IMPORTANT] Git Conditional Includes
[includeIf "gitdir:~/canvas/pers/"] path = ~/.gitconfig-pers [includeIf "gitdir:~/canvas/werk/"] path = ~/.gitconfig-werk
Templating and Logic
The .chezmoi.toml.tmpl file handles the initial setup by prompting for the machine's name:
{{- $machineName := promptStringOnce . "machineName" "machineName" .chezmoi.hostname -}}
[data]
machineName = {{ $machineName | quote }}
This value is stored in ~/.config/chezmoi/chezmoi.toml and used by my setup scripts to configure the hostname. I keep this minimal because I find Go's template syntax slightly cumbersome.
Additionally, .chezmoiignore ensures that the README.md, Brewfile, and Brewfile.lock.json stay in the source folder and are not copied to the home directory.
Bootstrapping a New Machine
Setting up a new Mac is now a streamlined process. First, I install Homebrew, then I execute these two steps:
- Install the manager:
brew install chezmoi - Initialize and apply:
chezmoi init --apply \
--promptString machineName=mini \
https://github.com/rednafi/dotfiles.git
The init command clones the repository, while --apply immediately deploys the files. The --promptString flag bypasses the interactive prompt for the machine name. Any scripts located within .chezmoiscripts/ are then handled accordingly.