What is Stow?
Stow is a convenient way to manage your dotfiles in MacOS and Linux. Whether it’s to back your configs up in general, version control them for iterating and/or testing, or to keep a consistent configuration between systems, Stow has you covered. Stow is a “symlink farm manager”, which centralizes files into “packages” within one central location which can be version controlled, then allows you to install (symlink) those files into their intended destinations or remove them with a simple stow command.
Stow was originally used for keeping track of system and per-user installations of software from source, and may well still be used for this purposes in some cases, but the need has been significantly mitigated by system package managers such as apt, yum, brew, and the like. There is less need for this particular purpose nowadays, but the need for config file management has arguably grown in its place.
Stow is pretty easy to install and use once you understand its conventions, but there are some caveats and a number of cases in which you may want to consider alternative approaches instead.
Why I’m Trying Stow
I’m far from being a linux or bash guru, but I do spend a lot of time in the shell / CLI. I also tend to split my time between multiple systems - a couple of macs (a mac mini and a laptop for travel and convenience) as well as a variety of linux systems such as misc home servers, VPS, WSL, and the like. I don’t necessarily use them all for the same things, but it would be nice for some configurations to be consistent between systems. I’m also learning to use Neovim more effectively, especially in regard to its plugin systems with a focus on development, and it’s important for me to be able to version control these experiments in case things go horribly wrong. I’d also like my TMUX to look consistently nice between systems. Stow to the rescue.
Process
Summary
Setting up Stow is pretty simple.
- Decide on a stow directory in which to keep your configs / dotfiles. I used
~/dotfilesfor mine. You can put READMEs, docs, bootstrap scripts, etc here. - Initialize the stow directory as a git repo (Optional, but recommended)
- Set up a
~/.stowrcwith a couple configuration defaults so you don’t have to include them in everystowcommand. (Optional). See the official docs / manpages for details for your use-case.
Now you’re ready to add/manage packages. Setup is that easy.
Adding packages to Stow is also pretty simple, but I should explain a bit about how stow organizes your files before we do it. Here’s a sample dotfiles/stow directory structure:
~/dotfiles/
├── README.md
├── bootstrap.sh
├── tmux/
│ └── dot-tmux.conf
└── nvim/
└── dot-config/
└── nvim/
├── init.lua
├── lazy-lock.json
└── lua/
├── config/
│ ├── autocmds.lua
│ ├── keymaps.lua
│ ├── lazy.lua
│ └── options.lua
└── plugins/
├── colorscheme.lua
├── editor.lua
└── lsp.lua
Here we have ~/dotfiles as our STOW_DIR / stow directory, which serves as the git repo and package storage location for
your config/dotfiles. There are a few things to note here:
- Files in the root of your stow directory are not associated with any package. They’re metadata, and can be documentation, bootstrap scripts, etc.
- Directories in the stow directory are Stow packages. The directory name is the “package” name used to manage
the contents with Stow. Note that the contents of the package are laid out exactly like they should be relative to
my home
~/directory, with one optional change I’ll discuss next. These MUST be laid out like this - it’s how stow will place them in your target directory (which by default is the parent of the stow directory, e.g.~in this case). - Note that in my example, I am using a
dot-prefix instead of.for my config files. This is an optional feature provided by stow that allows yourdotfilesdirectory / repo to not be full of hidden files/directories, but does create.symlinks. e.g.~/dotfiles/tmux/dot-tmux.confhas a symlink created at the location~/.tmux.conf. Similarly for the nvimdot-config->.configlink. This is purely optional, but I personally prefer it.
Ok, with that out of the way, let’s discuss how you add a package to stow. I’ll use tmux as an example, as it’s simply a single file.
# create the package dir
mkdir -p ~/dotfiles/tmux
# move your config file(s) or directories to the package, keeping the path structure correct
# I also backed up my files first, just in case as I'm experimenting with an unfamiliar tool
cp ~/.tmux.conf ~/.tmux.conf.bak
mv ~/.tmux.conf ~/dotfiles/tmux/dot-tmux.conf
# Now the magic, have stow create your symlink to the centralized / backed up conf file
stow tmux [--dir /Users/clay/dotfiles --dotfiles] # if not using a ~/.stowrc, ensure you properly set the stow dir arg
# Verify - you should see the symlink to your stow package file
ls -al ~/.tmux*
Now you can commit changes and sync between systems using git etc at your leisure.
To remove a stow symlink, you can use stow -D tmux for example
You can also configure Stow to ignore files, either in a specific directory/tree with .stow-local-ignore, or
via defaults in .stow-global-ignore, and of course there’s a cli option for it too. Keep in mind for ignores
however that they are not additive: If you use the cli option or the local dir file, the global will be ignored.
Be sure to include any relevant global ignore entries in your local or cli options, if they’re relevant.
My Experience
Stow is pretty simple once you wrap your head around it, but I initially had challenges with the package concept and
moving existing configuration to stow packages, mostly because I tried to abuse the --adopt flag, and it does NOT
work like I initially thought. It is not for initial setup, and as the docs will tell you, don’t use it unless you
know what you’re doing (and with adopt, I don’t).
Once I cleaned up my mess however and started fresh, I found it quite simple. I installed with brew install stow
and because I’m lazy and don’t want to have to add redundant arguments to all of my stow invocations, I set up my
.stowrc to include the 2 flags I would always use (omitting the comments):
--dir=/Users/clay/dotfiles # My Stow Dir
--dotfiles # I like the `dot-tmux.conf` style file naming in my repo, vs `.tmux.conf`
If you’re putting your Stow directory directly in your $HOME and you intend for all of your standard configs/packages
to be relative to your $HOME, you’re all set. This makes your $HOME the ’target’ directory, and being the parent
of the stow directory, that’s the default value.
If however you want to either put your dotfiles/stow dir elsewhere, not directly in the target dir, OR you just want to
be very clear and explicit about the target dir, you can also add --target=/Users/clay for example.
One of the other benefits of setting up your .stowrc is that you can then run stow ... from any location, and it
will work, as otherwise it assumes the current directory is the stow directory if you have neither the --dir option
in your command nor the STOW_DIR env var configured.
Stowing packages
I kept my initial onboarding with Stow simple. Once you’re set up, it’s not difficult to add new configurations as needed. I started with tmux (above) and nvim.
For tmux, I showed my process above:
mkdir -p ~/dotfiles/tmux
mv ~/.tmux.conf ~/dotfiles/tmux/dot-tmux.conf
stow tmux
ls -al ~/.tmux.conf
# verify it's the correct symlink
For nvim, it’s much the same:
# backing up my nvim dir... just in case
cp -R ~/.config/nvim ~/.config/nvim.bak
mkdir -p ~/dotfiles/nvim/dot-config # note the dot-config
mv ~/.config/nvim ~/dotfiles/nvim/dot-config/
stow nvim
ls -al ~/.config/n* # verify NVIM is present, dir symlinked to dotfiles
ls -al ~/.config/nvim/ # verify files are as expected
Syncing to a separate / fresh system
One the main reasons I was interested in Stow was to be able to sync configurations between systems. So it was time to try it. The process was pretty simple overall, starting with installing Stow and pulling my dotfiles repo.
brew install stow
# using the github cli, hopefully you know how to use git cli
gh repo clone dotfiles ~/dotfiles
Note that depending on your tools, there may be some post-“install” steps required. Both tmux and nvim required plugin updates in my case, as plugins were the whole point of my config syncing in the first place. Additionally while I had no tmux configuration on my destination system, I did have neovim installed with a separate / different wrapper flavor. I decided I like my original, so I backed up my nvim dir and just replaced it.
tmux
stow tmux
# Then see readme for TPM clone/update
My readme in my ~/dotfiles has details about how to set up TPM (git clone it to the correct plugin dir)
and how to pull get it to pull the other plugin updates. It took a couple of tries, but it worked and now tmux
looks nice on both my systems using the catppuccin/tmux plugin.
nvim
mv ~/.config/nvim ~/.config/nvim.bak
stow nvim
Since I’m using LazyVim, bringing it up to speed wasn’t difficult, but it too required a couple re-launches - Treesitter had some errors until I got it synced, closed and re-launched, and re-synced a time or two. Maybe I just wasn’t patient enough, but either way it’s working smoothly now on both systems, with identical plugins.
Stow is not great for shell rc files
Now I should mention of of my mistakes / misunderstandings.
One of the first packages I tried to configure in Stow was my zsh shell configuration, .zshrc. I wanted to have my
aliases, prompt configurations, etc all synced between my systems and it seemed like a good starting point.
I was wrong. You definitely can set up portions of your shell configuration in stow packages, but the shell rc
has a few caveats that at first I tried to work around, but eventually had to give up on.
- Much of the rc file is very machine specific, and has hard-coded paths and other system-specific details that would be inappropriate for a shared/synced configuration, potentially even between installations / wipes. Will I always re-install Flutter and have its bin in the PATH? Maybe not.
- Environment variables often have machine specific configs (per #1) but can also have sensitive data, such as secrets. You do NOT want your secrets (e.g. API or LLM keys, etc) in your git repo, especially if you have it set to public. Stow has no secret management. Secrets must stay out.
You can work around some of these issues, and as I said you can add parts of your shell configuration to stow.
For example, with zsh, you can use a .zshenv file for all of your env vars, and per my understanding they’ll even
be loaded before the main shell profile config files. Additionally, you can pull out aliases and other special
purposes sections into their own files and source them in your .*rc file. You can then add your aliases to a
stow package and sync that. Any secrets in aliases could be replaced with ENV VARs, and as long as you don’t commit
those, you’re golden. Some even argue that splitting your .*rc files into dedicated sourced component files is
a better practice, and it makes sense organizationally. It was too much work for me with minimal benefit for stow,
though I did move some of my ENV VAR configuration to .zshenv as I cleaned it up.
Troubleshooting / Considerations
Problems I encountered
I didn’t encounter many problems, other than incorrectly setting up the wrong symlink etc somehow with the adopt.
Don’t use --adopt unless you really understand it, and if you do I don’t know why you’re reading my stow beginner
post. Most fixes are simple - it’s all symlinks and directory organization, so if you have things in the wrong
directory in your stow package, resulting in incorrectly located symlinks, just stow -D {package} to remove the links,
move the files in the stow dir, then re-run stow stow {package}. It’s important to un-stow first, otherwise you
leave broken symlinks laying around, which isn’t great.
I did run into a couple configurations that I wanted to sync, but couldn’t because I had secrets (API keys etc)
in my .config/... files. Remember, secrets in your dotfiles repo is a no-go. There are ways around this, such
as having your secrets in a separate, ignored secrets.env file etc, linked in your stowed config, but I didn’t
dive into that just yet. It feels a bit janky, though maybe it’s really just a better practice. There are other
tools that may handle it better - I’ll take a look at Chezmoi at some point and compare the two tools, which work
in rather different ways. Stow is nice and simple, which was the main appeal for me.
General considerations
- Do Not Commit Secrets to Git: I’ve repeated this a few times, but it’s worth saying again - this is a big
security risk, even for private repos (maybe CoPilot will train on your secrets and use it as a sample someday,
who knows), and if your repo is ever exposed or made public, the history is still there even if you delete in
a later commit. You can rewrite git history, but you can’t rewrite how Github or whatever service you use
stores, caches, and otherwise operates on your data. Just never commit secrets.
If you do need secret management There are services and tools for this, and from what I understand Chezmoi fills an adjacent/overlapping niche, and allows for encrypted secret management. It’s more complex though, and I haven’t investigated it yet. May be worth looking into if you want your secrets synced, or you want default configs but with machine-specific variations. - Templating / Per-Machine Configs: Stow is basically one configuration, applied the same everywhere.
You don’t have to install all of the stow configs on every system, but what you use is the same everywhere. There are no templates, there are no per-machine configs. That’s the price of simplicity. As I mentioned in 1, Chezmoi might meet that need. - Stow doesn’t like mixed directory ownership: If you configure Stow as the owner of a config directory, it really doesn’t like having non-owned files there. I haven’t experienced this myself, as I just wanted the full directory, but there’s a lot of documentation related to tree folding, mixed ownership, etc, because it’s a bit of a mess from my understanding. Stow works best with full ownership of its packages / contents.
- Absolute symlinks and stowing symlinks: Stow uses relative symlinks, and it doesn’t make much sense to stow a symlink for syncing etc. Don’t add absolute symlinks, or symlinks in general to your package.
- Stowing to non-default target directories: When stowing to non-default targets, always use
-t/--target, including in bootstrap scripts. Otherwise you’ll end up dumping symlinks in the wrong place. - Don’t Stow Stow: Symlink recursion would cause all kinds of problems, but unless you’re trying to stow stow itself, this should be easy to avoid
- Cross-Platform Configurations: WSL symlinks work differently than macos/linux, and configurations themselves may vary between systems. You can have OS-specific packages to manage this, but keep an eye out when working cross-platform in general.
Conclusion
Stow is a useful, simple tool that effectively allows backups, version control, and related syncing of
configuration between systems, or even within a system, in a minimal, transparent manner.
The key word is simple however. This is often the key benefit, but if your needs are complex or you
want to manage secrets and the like, Stow may not be for you.
Check out Chezmoi or I’m sure there are other tools for your needs.