The Transition from Ansible to GNU Stow
In a previous post, I described my approach to managing my system by treating configuration as code.
Initially, I used Ansible for everything provisioning, application installation, and dotfile management. While that worked, it eventually became clear that Ansible was the wrong tool for managing frequently changing configuration files.
Ansible is excellent for provisioning: installing the OS, packages, system dependencies, and enforcing machine state. But dotfiles are different. They are iterative, experimental, and change often. Updating a complex playbook just to tweak a .zshrc alias introduced unnecessary friction.
So I split responsibilities:
- Ansible → Provisioning (OS + application installation)
- GNU Stow → Configuration (the actual dotfiles inside those applications)
The repository mentioned in the previous post has been updated to reflect this separation of concerns. It is now an Ansible-controlled framework for building and maintaining software on macOS, not a dotfile manager.
Why GNU Stow?
If you’re new to it, think of GNU Stow as a deterministic symlink manager.
Dotfiles are normally scattered across your home directory:
~/.zshrc~/.config/nvim/init.lua~/.config/ghostty/config- etc.
Tracking them with Git directly would mean either:
- Turning your entire home directory into a repository (messy and risky), or
- Manually copying files back and forth (error-prone and inconsistent).
GNU Stow solves this cleanly.
It allows you to:
- Centralize: Keep every configuration file inside a single directory (e.g.,
~/dotfiles). - Automate: Generate symbolic links automatically instead of creating them manually.
- Stay clean: If you stop using a tool, you can “un-stow” it and remove all associated symlinks without leaving residue behind.
Stow does one thing symlink management and does it predictably.
How It Works
The core idea behind Stow is directory mirroring.
Each application gets its own folder inside your dotfiles directory. Inside that folder, you recreate the directory structure relative to your home directory.
1) Structure Your Repository
Example:
~/dotfiles/
├── zsh/
│ ├── .zshrc
│ └── .zshenv
├── ghostty/
│ └── .config/
│ └── ghostty/
│ └── config
This structure mirrors:
~/dotfiles/zsh/.zshrc→~/.zshrc~/dotfiles/ghostty/.config/ghostty/config→~/.config/ghostty/config
Each top level folder (zsh, ghostty) is treated as a “package”.
2) Stow a Package
Navigate into your dotfiles directory:
cd ~/dotfiles
stow zsh
Stow examines the zsh folder and creates symbolic links in your home directory that point back to the canonical files inside ~/dotfiles.
To the system, the file appears to live in ~/.zshrc, but the canonical version lives in your Git repository.
Handling Existing Files
If a real file already exists at the target location, Stow will refuse to overwrite it. This is intentional it prevents accidental configuration loss.
You have two options:
- Move or delete the original file manually.
- Use:
stow --adopt zsh
The --adopt flag moves existing files into your dotfiles directory and replaces them with symlinks. This is especially useful when migrating an existing system into Stow.
Best Practices
One App per Directory
Avoid bundling multiple applications into a single folder. Keep tmux, nvim, zsh, and others separate so you can selectively stow configurations depending on the machine.
Use Git
Initialize a Git repository inside ~/dotfiles: git init
Now every configuration change is version controlled, reviewable, and portable across machines.
Your dotfiles become reproducible infrastructure without the overhead of a provisioning tool.
Be Explicit About the Target
By default, Stow assumes the target directory is the parent of your current location.
If your dotfiles are in ~/dotfiles and you run stow inside that directory, it will correctly target ~/.
If needed, you can specify:
stow --target="$HOME" zsh
Being explicit avoids surprises.
When Not to Use Stow
Stow is intentionally simple. That simplicity is its strength but it has limits.
You may need something more advanced if you require:
- Secrets management (consider
sopsor a password manager) - Complex templating
- Machine specific conditional configuration
- Cross platform dynamic substitutions
For pure symlink based configuration management, however, Stow is both transparent and reliable.
Why Not Other Tools?
There are alternatives like chezmoi or yadm that provide templating, encryption, and more advanced features.
I prefer Stow because:
- It has minimal abstraction.
- It is easy to reason about.
- It doesn’t hide behavior behind magic.
- It integrates seamlessly with plain Git workflows.
For my use case, simplicity beats feature richness.
Final Thoughts
Separating provisioning from configuration reduced complexity significantly:
- Ansible defines machine state.
- GNU Stow manages user-level configuration.
- Git tracks the canonical source of truth.
This separation of concerns gives me:
- Fast iteration on configuration changes
- Reproducible system builds
- Clean, reversible configuration management
- No unnecessary abstraction
The result is a workflow that is both predictable and frictionless.
Links
- My dotfiles repo.
- Mac Build for Development using Ansible Playbook repo.