macos unix

MANAGING DOTFILES WITH CHEZMOI


I work extensively in the command line, managing multiple servers and personal computers. To keep my workflow consistent, I rely on dotfiles - hidden configuration files that customize my system and applications. While many guides explain the basics of dotfiles, I won't be doing that here. This post is about how I use Chezmoi to keep mine in sync across all my machines.

For years, I managed my dotfiles with a handcrafted dotfiles management system. These scripts created symlinks across my filesystem, pointing to canonical files in a git repository. This worked well initially, but maintaining the sync scripts became increasingly complex over time as I added more and more features.

Enter Chezmoi, a command line tool that "Manages your dotfiles across multiple diverse machines, securely."

I evaluated several dotfile management tools against my key needs: security, flexibility, and ease of maintenance. Chezmoi stood out as the ideal solution because it offers these critical features:

Overview

This brief overview covers my Chezmoi setup in four main areas:

  1. Machine-specific configuration to handle different environments
  2. Template usage for dynamic file generation
  3. Shell configuration management
  4. Automation through script hooks

Each section includes practical examples from my actual setup that you can adapt for your own use. Everything I explain below (and more) can be seen in my dotfiles repository on Github.

Per-machine Configuration

When initializing Chezmoi on a new computer, I set a number of flags which are used in combination with the built-in chezmoi variables in my templates to manage machine-to-machine differences.

When placed at the top of your Chezmoi configuration file, these prompts ensure a smooth first-time setup and help prevent configuration errors. Instead of manually editing the config file, you'll be guided through the key decisions:

// Top of Chezmoi configuration file (.chezmoi.toml)

// The `promptOnce` functions only ask for values the first time you run Chezmoi, storing your answers for future use.
{{- $use_secrets := promptBoolOnce . "use_secrets" "Use secrets from 1Password? (true/false)" -}}
{{- $personal_computer := promptBoolOnce . "personal_computer" "Is this a personal computer for daily driving? (true/false)" -}}
{{- $homelab_member := promptBoolOnce . "homelab_member" "Is this computer in the homelab? (true/false)" -}}
{{- $dev_computer := promptBoolOnce . "dev_computer" "Do you do development on this computer? (true/false)" -}}
{{- $email := promptStringOnce . "email" "Email address" -}}

Templates

Templates are Chezmoi's secret weapon. They let you maintain one set of dotfiles that adapt automatically to different environments. This means you don't need separate configurations for each machine or complex BASH if/then statements that are parsed at runtime - the templates generate the right settings based on each system's needs.

The templates use Go's syntax, which looks complex at first but relies on a few basic concepts:

Here are some practical examples I frequently use that demonstrate its flexibility:

Boolean Logic

// If running on Darwin (macOS) AND homebrew is installed
{{ if and (eq .chezmoi.os "darwin") (lookPath "brew") }}
...

// If we are on a personal computer OR are using secrets
{{ if or (.personal_computer) (.use_secrets) }}
...

// If a homelabe member AND NOT a personal computer AND the CPU is amd64
{{ if and (.homelab_member) (not (.personal_computer)) (eq .chezmoi.arch "amd64" ) }}
...

Parsing Dictionaries and Lists

For example, if you have TOML file containing server information and want to write them to a a file.

# TOML file containing servers
[remote_servers]
    [remote_servers.one]
        name = "one"
        ip = 192.168.1.100
        include = true
    [remote_servers.two]
        name = "two"
        ip = 192.168.1.101
        include = false
// Echo the names and IP addresses of each server to ~/servers.txt
{{ range .remote_servers }}
    {{ if .include }}
        // A `$` must be pre-pended to global variables used within a range
        echo {{ .name }}={{ .ip }} >> {{ $.chezmoi.homeDir }}/servers.txt
    {{ end }}
{{ end }}

Conditionally Include Entire Files

If an entire template file is contained within an if statement, the file will only be written if the statement evaluates to true

{{- if eq .chezmoi.os "linux" }}
# This entire file will only be written to on computers running Linux
{{- end }}

ZSH and Bash Configuration

Many people store their shell settings in one large file (.zshrc or .bashrc). I take a different approach. By splitting my configuration into smaller files, I can:

These templates are then sourced into my shell environment at runtime using the following snippet.

Note: Zsh and bash specific files are given the extension .zsh or .bash respectively. Shared files are given the extensions .sh

# .zshrc
# (The code is identical in .bashrc except we look for .bash files)

# Files containing files *.zsh or *.sh to be sourced to your environment
configFileLocations=(
    # I use the XDG spec. These files are located in ~/.config/dotfile_source
    "{{ .xdgConfigDir }}/dotfile_source"
)

for configFileLocation in "${configFileLocations[@]}"; do
    if [ -d "${configFileLocation}" ]; then
        while read -r configFile; do
            source "${configFile}"
        done < <(find "${configFileLocation}" \
            -maxdepth 2 \
            -type f \
            -name '*.zsh' \
            -o -name '*.sh' | sort)
    fi
done

Script Hooks

Script hooks turn hours of manual setup into an automated process. When I set up a new machine, these scripts handle everything - from installing packages to configuring applications. The process is simple:

Scripts are run in alphabetical order and I number them (like run_before_00_homebrew.sh) to control their exact order.

You can view all my scripts on Github.

Scripts which run before syncing dotfiles

Scripts which run after syncing dotfiles


Want to try Chezmoi? Start with the official guide at chezmoi.io, then check out my dotfiles repository then explore my dotfiles repository for real-world examples. The time you spend setting it up will save you hours of configuration work later.

 → From Jekyll to Pelican