Dotfile setups tend to start as a repo full of files plus a shell script that copies them into $HOME. The copies drift from the repo, the script grows special cases, and eventually nobody is sure which version is real.

GNU Stow removes the copy step. One directory per tool, and stow <tool> symlinks its contents into the parent directory. Editing the repo edits the live config, because they are the same file.

The layout

Stow’s model is simple once you see it: each package directory’s contents are mirrored into the parent of the stow directory. So with the repo at ~/.dotfiles, the target is ~:

~/.dotfiles/
├── git/
│   └── .gitconfig                →  ~/.gitconfig
├── zsh/
│   ├── .zshrc                    →  ~/.zshrc
│   └── .zsh/                     →  ~/.zsh/
└── nvim/
    └── .config/nvim/             →  ~/.config/nvim/

Note the intermediate directories are part of the package. nvim/.config/nvim/ is what makes the symlink land at ~/.config/nvim. Then:

cd ~/.dotfiles
stow git zsh nvim

Adding a new tool is a directory and one command. Removing one is stow -D <tool> — the symlinks go, the repo keeps the config.

Conflicts

The one thing that will bite you: Stow refuses to overwrite a real file.

WARNING! stowing git would cause conflicts:
  * existing target is neither a link nor a directory: .gitconfig
All operations aborted.

That is Stow declining to destroy a config you may not have copied anywhere. Two ways out, and the choice matters:

  • stow --adopt git — moves the existing ~/.gitconfig into the package, then links it back. Your live config wins, and the repo now contains it. Check git diff immediately after: adopt overwrites the repo’s version, so this is exactly when you find out whether the two had diverged.
  • Delete the target firstrm ~/.gitconfig && stow git. The repo’s version wins. Only do this once you have confirmed the live file has nothing you want.

Both are fine. Picking the wrong one silently is not, so look at the file before deciding.

Folding, and the surprise it causes

When a target directory does not exist, Stow symlinks the directory rather than creating it and linking each file — this is called folding. It keeps the tree tidy, but it means ~/.config/nvim is a link to the package, so anything written into it by another program lands in your repo.

For config directories where a tool writes its own state alongside your config, that is not what you want. stow --no-folding <tool> creates real directories and links only the files.

Why it holds up

Deploying config is a solved problem the moment you stop copying files. A new machine is git clone plus one stow per package. There is no sync step to forget, no “did I update the repo after fixing this?” because there is only one file.

The catch is that Stow does exactly one thing: it makes symlinks. It will not install packages, set macOS defaults, or bootstrap a machine. That is a feature — those belong in their own script, where you can read them — but it does mean Stow is the deployment layer of a dotfiles setup, not the whole thing.