Replacing My Dotfiles Manager with a 50-Line Shell Script
I’ve used Chezmoi, Stow, Yadm, and a custom Makefile at various points. Each time, the tool would accumulate complexity that had nothing to do with my actual problem: keeping config files in sync across machines.
Last month I deleted all of it and replaced it with a shell script. Here’s the whole approach.
The Problem With Dedicated Tools
Dotfiles managers solve hard problems well — templating, secrets management, cross-platform conditionals. But if you don’t need those things, you’re importing a large mental model for no reason.
My requirements were simple:
- Symlink files from
~/dotfilesto their correct locations - Handle a handful of platform differences (macOS vs Linux)
- Be readable by future me after 6 months away
Chezmoi is excellent software. It wasn’t a fit for my requirements.
The Script
#!/usr/bin/env bash
set -euo pipefail
DOTFILES="$HOME/dotfiles"
OS="$(uname -s)"
link() {
local src="$DOTFILES/$1"
local dest="$HOME/$2"
mkdir -p "$(dirname "$dest")"
ln -sf "$src" "$dest"
echo " linked $2"
}
echo "Installing dotfiles..."
# Shell
link "zsh/zshrc" ".zshrc"
link "zsh/zshenv" ".zshenv"
# Editor
link "nvim" ".config/nvim"
# Git
link "git/config" ".gitconfig"
link "git/ignore" ".gitignore_global"
# Tmux
link "tmux/tmux.conf" ".tmux.conf"
# Platform-specific
if [[ "$OS" == "Darwin" ]]; then
link "macos/karabiner" ".config/karabiner"
fi
echo "Done."
That’s it. ln -sf handles the “already exists” case by overwriting. set -euo pipefail makes failures loud. The whole thing is readable at a glance.
Directory Structure
dotfiles/
zsh/
zshrc
zshenv
nvim/
init.lua
lua/
git/
config
ignore
tmux/
tmux.conf
macos/
karabiner/
install.sh
The structure mirrors the destination. There’s no mapping file to maintain — if a file should live at ~/.config/nvim/init.lua, it lives at dotfiles/nvim/init.lua.
Handling Secrets
I don’t put secrets in dotfiles. API keys and tokens live in ~/.env.local which is sourced at shell startup. That file isn’t symlinked — it’s machine-local and never touches version control.
# .zshrc
[[ -f "$HOME/.env.local" ]] && source "$HOME/.env.local"
For anything that genuinely needs templating (like a git config with different emails per machine), I just keep two files:
git/
config.work
config.personal
And the install script checks a $MACHINE_PROFILE environment variable set in .zshenv.
What I Gave Up
Automatic updates across machines — with Chezmoi I could run chezmoi update from anywhere and pull new dotfiles. Now I git pull in the dotfiles directory and re-run install.sh. Two commands instead of one. Fine.
Encrypted secrets — I don’t use this feature anyway.
Age-encrypted files on a remote — not my threat model.
Three Months In
The script has needed zero changes. When I set up a new machine, I clone the repo and run ./install.sh. That’s the whole onboarding flow.
The best tool is the one you understand completely. A shell script you wrote is hard to beat on that metric.