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 ~/dotfiles to 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.