← Back to news

Migrating from GNU Stow to Chezmoi

rednafi.com|12 points|2 comments|by speckx|Jun 18, 2026

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:

  1. MacBook Pro: Dedicated to professional work.
  2. MacBook Air: Used for personal projects.
  3. 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.

FeatureGNU Stow Experience
Learning CurveExtremely low; very intuitive.
OperationIdempotent commands.
EditingDirect write-through via symlinks.
Pain PointDirty 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 ~/.zshrc copies the file into the source directory as dot_zshrc.
  • Adding a nested file like ~/.config/gh/config.yml creates dot_config/gh/config.yml.
  • I rely on chezmoi add to 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 060080600_8).
  • .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.sh
      • run_onchange_after_init-macos-machine.sh.tmpl
      • run_onchange_before_install-homebrew-bundle.sh.tmpl
  • .gitignore
  • Brewfile
  • README.md
  • dot_agents/skills/ (go-modernize, go-styleguide, meatspeak)
  • dot_claude/ (settings.json, symlink_skills.tmpl)
  • dot_codex/private_config.toml
  • dot_config/
    • gh/ (config.yml, private_hosts.yml)
    • ghostty/config
  • dot_gitconfig
  • dot_gitconfig-pers
  • dot_gitconfig-werk
  • dot_shellcheckrc
  • dot_zsh_aliases
  • dot_zshrc

Dotfiles Concept

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.