Notes

Migrating from Manually Managed Dotfiles to Chezmoi (Ongoing)

#Misc

Updated: September 4, 2025

Notes

chezmoi is a dotfiles management tool, but it does not work by simply placing symbolic links. It can make a simple dotfiles setup unnecessarily complicated, so it should be introduced with some care.

Goal

To support additional Ubuntu machines, I am migrating from manually managed dotfiles (~/dotfiles deployed into my home directory via symbolic links, together with shell scripts) to a unified, cross-platform setup using chezmoi.
The final goal is to manage both Ubuntu and macOS configurations in a single repository.

Current Setup

Before introducing chezmoi, my dotfiles repository looked roughly like this:

~/dotfiles/
├── home # configuration files
├── scripts # shell scripts
└── others

It stored dotfiles and scripts for operating them. Files under home were deployed into $HOME through symbolic links.

This structure is simple, but

  • branching by OS (split with if statements),
  • managing secrets such as SSH keys and tokens,
  • and automating initialization on new machines all looked annoying enough that I decided to move to chezmoi. (It is not strictly necessary, but I also wanted to learn how it works.)

What Is Chezmoi?

chezmoi is a dotfiles management tool written in Go.

Its main features include cross-platform support, templating, and encrypted secret management.

Since it handles dotfiles management for you, it saves you from writing your own shell-script-based setup from scratch.

Migration Flow

Bring in the Repository

chezmoi init $GithubUserName

This clones $GithubUserName/dotfiles.git into $HOME/.local/share/chezmoi.

Then delete the configuration files under chezmoi/home as a cleanup step. (This is only the directory cloned by chezmoi, so it does not affect the original repository itself.)

rm -rf $HOME/.local/share/chezmoi/home/.*

Change the Source Directory

By default, chezmoi expects dotfiles to live at the root of the repository, but I want them under home, so:

chezmoi cd
touch .chezmoiroot

Create .chezmoiroot and change the source directory. Its contents are simply:

home

Import Existing Dotfiles

chezmoi add --follow ~/.bash_profile
chezmoi add --follow ~/.zshrc
chezmoi add --follow ~/.zprofile
chezmoi add --follow ~/.profile
chezmoi add --follow ~/.gitconfig
chezmoi add --follow ~/.vimrc
chezmoi add --follow ~/.clang-format
chezmoi add --follow ~/.condarc
chezmoi add ~/.config/karabiner
chezmoi add ~/.config/nvim

chezmoi add <file> places the specified file under chezmoi management. Since my home directory already contains symbolic links, I use the --follow option to import the actual target file behind each link. (For directories, however, --follow cannot be used.)

Import Secrets

I use Bitwarden, so I integrate Bitwarden CLI with chezmoi to manage secrets in encrypted form. See the official documentation for details.

brew install bitwarden-cli # on macOS
bw login # log in to Bitwarden
bw unlock # unlock Bitwarden if prompted

After unlocking, you get a session key, which should be set as the BW_SESSION environment variable:

export BW_SESSION="xxxxxxxxxxxxxxxx"

With this session key set in the environment, chezmoi can retrieve secrets from Bitwarden.

Incidentally, if you write the following in your chezmoi configuration (chezmoi.toml or .json), chezmoi will automatically call bw unlock when BW_SESSION is not set:

{
  "bitwarden": {
    "unlock": "auto"
  }
}
chezmoi secret bitwarden init

Result:

$HOME/.local/share/chezmoi/
└── dot_zshrc

For more information about chezmoi, see the official documentation.

If you already have symbolic links pointing to ~/dotfiles,
chezmoi add ~/.zshrc will follow the link and import the real file,
so you can run add without deleting the link first.

Migration flow:

chezmoi init
chezmoi add ~/.zshrc
chezmoi add ~/.config/nvim
chezmoi diff
chezmoi apply

After the migration is complete, remove the symbolic links that are no longer needed:

find $HOME -type l -lname '*dotfiles*' -delete

Branching by OS

Use Chezmoi’s template mechanism (.tmpl).

Example: dot_zshrc.tmpl

{{ if eq .chezmoi.os "darwin" }}
# macOS-specific
export PATH="/opt/homebrew/bin:$PATH"
{{ else if eq .chezmoi.os "linux" }}
# Linux-specific
export PATH="/usr/local/bin:$PATH"
{{ end }}

When chezmoi apply runs, the content is expanded automatically according to the OS.

Separating Secrets

chezmoi can manage secret files in encrypted form using the encrypted_ prefix:

chezmoi add --encrypt ~/.ssh/config
chezmoi secret add github_token

Encryption can be handled with either GPG or age.

Unified Structure for Ubuntu and macOS

An example of the final structure of $GithubUserName/dotfiles.git:

dotfiles/
├── dot_zshrc.tmpl
├── dot_gitconfig
├── private_dot_ssh/
   └── config.age
├── run_once_install-packages.sh.tmpl
└── README.md
  • run_once_*.sh are setup scripts that run automatically on first execution.
  • By embedding OS branching in .tmpl files, the same repository can reproduce both environments.

Workflow

  1. Set up a new environment:

    chezmoi init $GithubUserName
    chezmoi apply
  2. Apply changes:

    chezmoi edit ~/.zshrc
    chezmoi diff
    chezmoi apply
  3. Push to GitHub:

    chezmoi cd
    git add .
    git commit -m "Update macOS zshrc"
    git push origin main

Future Extensions

  • Automatically install brew/apt packages with run_once scripts
  • Branch inside templates based on environment variables
  • Roll dotfiles forward with chezmoi update

Comments