Taking Control of My Dotfiles with chezmoi
#howto #development #recommendation
Managing dotfiles is one of those things that starts as a quick weekend project and slowly evolves into a serious hobby. I’ve spent more hours than I’d like to admit tweaking my terminal colors or perfecting my shell aliases.
For a long time, I relied on a complex shell script to handle everything. It would move config files around, install software, and set up plugins. It worked fine when I only had one machine, but I started switching between macOS and various Linux distros, and my “simple” script became a maintenance nightmare. My goal was to have (mostly) identical environments on all my machines.
That’s when I found chezmoi [1]. While it didn’t necessarily make my setup less complex, it made the whole mess significantly more maintainable.
What is chezmoi?
At its core, chezmoi is a manager for your dotfiles. Instead of just symlinking files, it uses a 1:1 mapping between a source directory (usually a git repo) and your home directory.
The real magic is in the templating. It allows you to use logic inside your configuration files. If I’m on my MacBook, I might want a specific alias setup or a different PATH variable than when I’m on my Linux server.
Commonly used functions like lookPath (to check if a command exists) and OS-specific conditionals make your configs truly portable:
1# .gitconfig.tmpl
2[core]
3{{- if lookPath "nvim" }}
4 editor = nvim
5{{- else }}
6 editor = vim
7{{- end }}
It’s equally easy to handle different system paths or configurations between macOS and Linux. For example, setting up Homebrew in my .zshrc.tmpl:
1{{- if eq .chezmoi.os "darwin" }}
2if [ -f "/opt/homebrew/bin/brew" ]; then
3 eval "$(/opt/homebrew/bin/brew shellenv)"
4fi
5{{- else if eq .chezmoi.os "linux" }}
6if [ -f "/home/linuxbrew/.linuxbrew/bin/brew" ]; then
7 eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
8fi
9{{- end }}
With chezmoi, I can use a single file and let the template engine handle the differences based on variables like .chezmoi.os or custom data defined in my profile.
Making it Public
I decided to host my dotfiles publicly on GitHub [2]. I’ll be honest: my setup probably won’t suit anyone else. It’s tailored exactly to how I work. However, keeping it public means I can pull my entire environment onto any new computer in seconds.
To make things even easier, I wrote a small wrapper script that allows me to kick off the setup on a remote machine via SSH. It’s very convenient when I’m setting up a new VPS or a headless home server.
1# setup-chezmoi-remote.sh
2cat "$TEMP_CONFIG" | ssh "$SSH_TARGET" "
3 sudo apt-get update && sudo apt-get install -y curl git
4 mkdir -p ~/.config/chezmoi
5 cat > ~/.config/chezmoi/chezmoi.toml
6 sh -c \"\$(curl -fsLS get.chezmoi.io)\" -- init --apply --force $CHEZMOI_REPO
7"
Handling the Secret Stuff
Security is always the tricky part with public dotfiles. I use gopass [3] to manage my sensitive configuration files and passwords/keys. While chezmoi has built-in password management, I found it didn’t quite fit my needs because I also need to deploy certain binary files that are stored securely.
My workflow looks like this:
Initialize: I run
chezmoi init. This triggers arun_once_beforescript that asks for a profile.1# run_once_before_10-setup-config-from-gopass.sh.tmpl 2echo "Available config profiles:" 3echo " 1) basic - Minimal setup" 4echo " 2) dev - Full dev environment (Linux)" 5echo " 3) mac - macOS workstation" 6 7read -r choice 8GOPASS_PATH="config/chezmoi/${CONFIG_NAME}.toml" 9gopass fscopy "$GOPASS_PATH" "$CONFIG_FILE"Profile Selection: The script asks me for a profile (like
mac,dev, orbasic).Fetch Config: It pulls the matching configuration directly from my gopass store.
Apply: I run
chezmoi apply.
I also added a custom script routine that fetches other sensitive files from gopass based on a local configuration file.
1# run_after_25-copy-gopass-files-local.sh.tmpl
2while IFS=: read -r gopass_path dest_path; do
3 dest_path="${dest_path/#\~/$HOME}"
4 if gopass fscopy "$gopass_path" "$dest_path" 2>/dev/null; then
5 chmod 600 "$dest_path"
6 success "Copied: $gopass_path → $dest_path"
7 fi
8done < "$CONFIG_FILE"
Once those are in place, chezmoi takes over.
Automation and Tooling
The rest of the heavy lifting is handled by chezmoi’s lifecycle scripts: run_before, run_after, and run_once.
I use these to automate my entire toolchain installation. On almost every system, I use Homebrew [4] to manage packages. My scripts check which tools are missing and install them based on the profile I selected, then chezmoi configures them. It’s a “set it and forget it” system that actually works.
If you’re still struggling with a giant install.sh script that breaks every time you update your OS, I highly recommend giving chezmoi a look. It takes some time to migrate, but the peace of mind is worth it.
Sources
[1] chezmoi - https://www.chezmoi.io/
[2] cloonix/dotfiles - https://github.com/cloonix/dotfiles
[3] gopass - https://www.gopass.pw/
[4] Homebrew - https://brew.sh/