Stowing My Dotfiles
2026-04-06
I’ve had a Git repository storing my dotfiles for well over a decade at this point, and I’ve had the same installation process for just about all of that time: A script/bootstrap that I originally copied from Zach Holman’s dotfiles, that works like this:
- It crawls the repository for files with a name
*.symlink. - It processes each found filename thusly:
- Remove
.symlink - Remove the first directoy name (this allows config files to be organised per topic)
- It adds a
.to the start
- Remove
- It creates a symlink from
~/$processedNameto the original found file or directory.
This was perfect for traditional “dotfiles”, which are so named because they are configuration files in the root of a user’s home directory that begin with a dot.
XDG Took Over The World
The freedesktop organisation produce a series of specifications under the XDG (Cross-Desktop Group) moniker. One such specification is the XDG Base Directory Specification, which was first published in 2003. It contains within it advice for where applications should store configuration and runtime files such that they do not pollute a user’s home directory with an inconsistent spattering of hidden files. I haven’t been able to find much on the history of this specification, but my memory is that this was it was an obscurity until the spec was updated in 2010. This update coincides with the development of Systemd, which went pretty hard in supporting this, and I think is a large part in generating broad support for this spec.
Slowly, the tools I was using began to adopt the XDG base directory specification, or at least began to support it. For example, Git added support alongside their existing configuration files in 2012. My first exposure to this was Fish, which expected its configuration to live in ~/.config/fish/config.fish, and the next was Neovim that expected its configuration to live in ~/.config/nvim/init.vim. To start off, I just manually made symlinks from these directories to the relevant part of my dotfiles repository. It was surprising to me that I tolerated this for well over half a decade, but I think that speaks more to how often I set up a brand new user account than anything else.
But two years ago, I finally decided to adopt this specification myself, and give it first-class status. I updated script/bootstrap.sh such that files with .xdg.symlink would actually get mapped to ~/.config/$baseName. This has been in place since then and I don’t think I ever revisited it. Over time, however, I noticed something surprising: everything I was maintaining was an .xdg.symlink type installation, rather than a traditional “dotfile”. The workaround had become the standard path.
The other XDG Variable
I regret to tell you that I have hit the limits of my little workaround script. You see I only link things from ~/.config/*, which is the default value for XDG_CONFIG_HOME. However, if you followed any of the links to the XDG Base Directory Specification above (or if you’re just clever), you will know that there are other locations specified for different user-specific state. The pertinent one in this story is XDG_DATA_HOME. Here is what the spec says about it:
$XDG_DATA_HOMEdefines the base directory relative to which user-specific data files should be stored. If$XDG_DATA_HOMEis either not set or empty, a default equal to$HOME/.local/shareshould be used.
Gee, thanks for clearing that one up. This is a little underspecified for my tastes, but it seems to be used mostly for things like history files. Lazy.nvim also stores downloaded plugins there. In short, it’s full of stuff that doesn’t need to be committed and is not important. Except for one thing.
On my desktop, I use the Sway window manager, and an assortment of ancillary tools that together form something like a desktop environment. All of these support theming, but none of them support consistent or automatic theme switching that I might get in something like KDE. I like to use a light theme during the day, and a dark theme during the evening or in other low-light situations. The solution to this exact problem is another little ancillary tool called Darkman. To get all of these different things working together nicely requires some custom scripting. Darkman provides hooks for these:
darkmancan run custom executables (which can be simple shell scripts).Scripts are searched in a directory named darkman inside paths defined in the
XDG_DATA_DIRSas well asXDG_DATA_HOME. Each script receives the current mode (“dark” or “light”) as its first argument ($1).
These scripts absolutely belong in my personal dotfiles, although I can see why these are treated as state rather than configuration by Darkman. The solution, therefore, is to be able to symlink from ~/.local/share/darkman to somewhere in my repo.
Perhaps darkman.xdg-data.symlink?
Enter GNU Stow
Absolutely not. Enough is enough. I was considering options to evolve this script into something a little more sophisticated, was turning over ideas in my head, playing with different constraints and compromises. Before progressing any further with such a project, I wanted to look at some existing art, to see if maybe there was an existing tool that did what I wanted, or perhaps gave me an alternative way of looking at it. After all, I’ve been using the same thing for a good portion of my life at this point.
I know that some people have used Stow for this. Stow was initially designed to symlink components of individually packaged software into system level directories so that it can more easily be used. I’m pretty sure it’s mostly a set of Perl scripts, and it was initially released before I was born, so it’s all green flags so far. Installing packages like this is a very similar job to installing dotfiles into a user’s home directory, and Stow makes this even more similar by the assumptions it makes at the outset: crucially, Stow will symlink things from the source directory into the same relative locations in its immediate parent. This means that if my dotfiles are cloned to ~/dotfiles, then by default, Stow will put things in ~. Hell yeah.
Stow has added more affordances for dotfiles in particular through its life. The most obvious of these is a --dotfiles flag which — while linking — rewrites directory names from dot-* to .*, meaning that ones dotfiles repo need not be a mess of hidden files.
After a careful read of the manpages, I was able to replace the venerable and now erstwhile script/bootstrap.sh with a new, sleek, modern, fresh, tasty, install.sh that does little other than call stow:
#! /usr/bin/env bash
function main {
stow \
--verbose \
--stow . \
--target ~ \
--restow \
--dotfiles \
--ignore "\.DS_Store" \
--ignore "install.sh" \
--ignore "bin" \
--ignore "accessories"
}
mainI chose to abandon the top-level topic directories, and keep my dotfiles repo as a partial mirror of my home directory. The reasoning here is that since I pretty much exclusively use XDG-compliant software, that topic distinction already exists within the dot-config and dot-local/share directories. While I no longer use Zsh, I wasn’t quite ready to eradicate its config, so it lives in dot-zsh and dot-zshrc, but that is [a] rare enough that I don’t really feel the need to solve this at a systems level, and [b] obviously named enough that nobody should be confused by this.
I’m delighted that in 2026, I’m able to pick up an ancient piece of tooling, and over the course of a morning, totally rearrange my dotfiles to accommodate a new need. In a career that is increasingly uncertain in a world that is increasingly bleak, I have found that silly and nerdy things like this bring me a load-bearing amount of joy.