One dot-config Repository for two Notebooks
For a while my ~/.config on my main notebook has been a git repository, pushed to my private Forgejo.
Now I wanted the same repository on my travel notebook.
The catch: the notebooks are not identical.
The main notebook runs sway on Wayland, the travel notebook runs i3 on X11.
And some configs are simply different on each machine, on purpose.
Adopting the repository on the second notebook
I initialized git inside the existing folder on the travel notebook and pointed the branch at the remote without touching any files:
cd ~/.config git init -b main git remote add origin ssh://git@forgejo/mfa/dot-config.git git fetch origin git reset origin/main
After the git reset the working tree is unchanged, but git status shows exactly how this machine differs from the repository.
The repository uses a deny-by-default .gitignore (first line is /*, then a !/name line for every config that is actually versioned), so all the application caches and state folders stay invisible.
Most differences were easy to sort:
window manager configs don't conflict because sway/ and i3/ are different folders, so both live in the repository and each machine only uses its own.
Some files existed only in the repository and were safe to check out.
Some local drift was not worth keeping (btop, htop, mc) and I reset it to the repository version.
And some files I stopped tracking completely: window geometry state like git/gitk and pavucontrol.ini, and uv/uv.toml.
But two files remained that genuinely need different content per machine:
alacritty/alacritty.toml is tuned per display server and screen,
and atuin/config.toml has a different sync_address, because the main notebook reaches the Atuin server over the LAN IP, while the travel notebook uses always the Tailscale address.
The per-host setup with links.conf and host.d
Same path, different content per machine — git can't do that on one branch. Something has to pick the right file on each machine.
I decided to do this with a manifest and a small POSIX script, both versioned in the repository.
The manifest links.conf lists which targets are per-host:
# target ← per-host source ($HOST substituted); applied by ./link.sh alacritty/alacritty.toml alacritty/host.d/$HOST.toml atuin/config.toml atuin/host.d/$HOST.toml
And link.sh creates a symlink for every line:
#!/bin/sh # link.sh — create per-host symlinks defined in links.conf set -eu cd "$(dirname "$0")" HOST=$(hostname) while read -r target source; do case "$target" in ''|'#'*) continue ;; esac source=$(printf '%s' "$source" | sed "s/\$HOST/$HOST/") [ -e "$source" ] || { echo "skip $target (no $source)"; continue; } ln -sfn "$(realpath --relative-to="$(dirname "$target")" "$source")" "$target" echo "$target -> $source" done < links.conf
The real files live in a host.d/ folder next to the config, named by hostname, and both variants are tracked:
The symlink itself is in the .gitignore.
After a clone or a pull that adds new per-host files, running ./link.sh once wires everything up.
Why this and not a dotfile manager? Because the whole "tool" is 12 lines of shell and a 2-line manifest, both live in the repository, and there is nothing to install on a new machine. If the manifest ever grows past a dozen lines, it is the exact specification of which features I would need from a real tool.
One caveat: this only works for configs that are hand-edited or where the application writes in place. An application that saves via write-temp-then-rename replaces the symlink with a regular file.
Alternatives I looked at
There are plenty of dotfile managers that solve the per-machine problem.
yadm was the closest match.
Its "alternates" feature does natively what link.sh does: track alacritty.toml##hostname.foo and yadm symlinks the right one on checkout.
But yadm wants $HOME as its worktree, so my ~/.config-rooted repository would need a path migration, and it brings encryption, templating, bootstrap hooks — a lot of tool for one symlink.
toml-bombadil has profiles, which is the right idea. But it renders every file through its template engine into a shadow directory and symlinks from there, so editing a config means re-linking afterwards. I wanted the profiles without the templating.
chezmoi is templating-first: one file with {{ if eq .chezmoi.hostname ... }} blocks instead of two files.
Powerful, but the config in the repository is no longer the config on disk, and live editing is gone.
GNU stow is nicely minimal, but it symlinks from a package farm into ~/.config.
My repository is ~/.config, and I didn't want to restructure everything into packages.
A branch per machine needs no symlinks at all, but means merge discipline forever, and in my experience those branches drift apart and rot.
For two notebooks and (currently) two per-host files, the shell script wins.

