Migrating my dotfiles to chezmoi: one config for a work desktop and an AI-harness-testing laptop
TLDR; I migrated my dotfiles into a single chezmoi-managed repo at github.com/stevecrawshaw/dotfiles. One source tree is rendered per machine by Go templates: a Windows 11 work desktop (bash, Git Bash) and a Lenovo T490 Ubuntu laptop (zsh, Ghostty) that exists mainly to test AI coding harnesses like DeepSeek and Pi.
Why I needed this
For years my configuration lived in roughly three places, none of them authoritative:
- a
.bashrcfor the work desktop, hand-edited over several jobs’ worth of cruft - a
.zshrcon the new Ubuntu laptop, written from scratch and already diverging ~/.claudeas its own git repo — which was fine until I started building skills, agents and hooks, and the repo stopped being a clean mirror of reality
The trigger was the laptop. I’d just set up a Lenovo T490 with Ubuntu as a dedicated testbed for AI coding harnesses, and within a month the two machines’ configs had drifted so far that “copy the file across” wasn’t working anymore. Editor settings, shell aliases, git config — everything was subtly different, and nothing was the source of truth.
The laptop’s job: an AI harness testbed
It’s worth being explicit about what the personal laptop is for, because it shaped the whole migration. The laptop is a testing ground for AI coding harnesses — DeepSeek, Pi, Claude Code, whatever new coding agent shows up next. I install a harness there first, point it at my repos, and run it against real tasks before trusting it anywhere near work.
That role drives three decisions in the dotfiles:
- The entire
~/.claudetree is version-controlled — agents, skills, hooks, commands, rules. When an experiment breaks something (and they do), I can diff against the last good state and re-provision in seconds instead of reconstructing it from memory. - The shell is tuned for watching agents work — fast zsh startup, fzf for history and file jumping, an oh-my-posh prompt with Nerd Font glyphs in Ghostty. The terminal is the dashboard for the experiments.
- The bootstrap provisions exactly what a coding harness needs on a fresh machine:
uv,gh,ripgrep,fzf, git, plus the R and Python toolchains my data work needs. A harness is useless if it can’t run code.
The joke is that the migration I’m writing about is because of the AI testing, not in spite of it — version-controlled config is what makes harness experimentation reversible.
Why chezmoi
I considered the usual options first:
- A bare git repo with symlinks — the classic approach, but symlinks need privileges on Windows, which is a whole conversation I didn’t want to have with the IT department.
- GNU Stow — same symlink problem, and no templating.
- Dotbot or a custom script — reinventing the wheel, badly.
chezmoi won on three concrete points:
- It copies files rather than symlinking them. This sidesteps the Windows symlink-privilege problem entirely. No admin rights, no developer mode, no junction-point hacks.
- Go templates. One source file can render differently per machine — the gitconfig’s
core.editorpath andautocrlfsetting, the OneDrive path on the work box, machine-specific PATH entries. Machine identity is derived from the OS, so there’s nothing to prompt for. run_onchange_*scripts. Provisioning is declarative and idempotent: install packages before dotfiles are written, replay editor extension manifests when they change. Re-running is safe.
The repo layout
.chezmoi.toml.tmpl generates per-machine config on `chezmoi init`
.chezmoiignore per-OS exclusions (target paths, not source names)
.chezmoitemplates/ shared content included by several targets
vscode-settings.json the real VS Code settings
positron-settings.json the real Positron settings
dot_bashrc.tmpl -> ~/.bashrc (Windows)
dot_zshrc.tmpl -> ~/.zshrc (Ubuntu)
dot_gitconfig.tmpl -> ~/.gitconfig
dot_config/shell/aliases.sh.tmpl -> ~/.config/shell/aliases.sh (both shells)
dot_config/shell/env.sh.tmpl -> ~/.config/shell/env.sh (both shells)
dot_config/ghostty/config -> ~/.config/ghostty/config (Ubuntu)
dot_claude/ -> ~/.claude (Claude Code config)
AppData/Roaming/Code|Positron/ -> editor config (Windows)
dot_config/Code|Positron/ -> editor config (Ubuntu)
*-extensions.txt extension manifests, replayed by run_onchange_
run_onchange_* provisioning scripts
A few things worth calling out:
- The
dot_prefix is chezmoi’s convention — a source file calleddot_zshrc.tmplbecomes~/.zshrc. - Editor settings live in exactly one place. VS Code reads from
%APPDATA%\Code\Useron Windows and~/.config/Code/Useron Linux, so both trees exist in the repo — but each is a one-line file that includes the same shared template from.chezmoitemplates/..chezmoiignoreexcludes the irrelevant tree per OS. - What’s templated is only what actually diverges — aliases (OneDrive paths,
start msedge, winpty wrappers are work-only), env (R/rv paths), gitconfig (autocrlffalsevsinput, the gh credential helper on Linux only). VS Code keys with.windows/.linuxsuffixes are left untemplated because VS Code selects those at runtime. ~/.claudeis the biggest subtree — ten agents, thirty-plus skills, hooks, commands and rules. That’s the AI-testing payload riding along with the shell config.
Bootstrapping the Ubuntu laptop
The whole point of a dotfiles repo is that a new machine gets set up without you remembering anything. The laptop bootstrap is three commands:
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"
chezmoi init --apply --source="$HOME/projects/dotfiles" https://github.com/stevecrawshaw/dotfiles.git
chezmoi init generates ~/.config/chezmoi/chezmoi.toml from .chezmoi.toml.tmpl, deriving machine = "personal" from the OS (the work box gets machine = "work" from Windows). Because the run_onchange_before_00-ubuntu-packages.sh script runs before any dotfiles are written, the shells and tools the rc files expect already exist:
- apt packages — zsh, git, fzf, ripgrep, build tooling, R,
gh(from GitHub’s own apt repo, because Ubuntu’s build lags) uv, radian (the R console VS Code uses), oh-my-posh, and the JetBrainsMono Nerd Font the prompt theme needs- chezmoi itself, plus reminders to run
gh auth loginandchsh -s zsh
The script is honest about what it can’t do: VS Code, Positron and Ghostty need manual downloads (it prints the links). Install those, run cza again, and the extension-sync script picks up the editor CLIs and replays the manifests.
Daily workflow
The aliases make the loop short enough to be habit:
czd # chezmoi diff -- what would change on this machine
cza # chezmoi apply -- write source -> $HOME
czu # chezmoi update -- git pull + apply. The everyday sync.
czs # chezmoi status
czr # chezmoi re-add -- pull edits you made directly in $HOME back to source
czcd # cd to this repo
aka # edit the shared aliases file
The typical loop after changing something:
czr && czcd && git add -A && git commit -m "feat: ..." && git push
# then on the other machine
czu
czr is the alias I use most, because the AI harnesses edit config too. When Claude Code adds a skill or tweaks a rule under ~/.claude, that edit starts life in $HOME — czr pulls it back into source so the next czu on the other machine picks it up.
Secrets
Nothing in the repo is encrypted, so nothing secret goes in it. Per-machine secrets live in ~/.env.local, which is untracked and sourced by both .bashrc and .zshrc if present:
# ~/.env.local
export SOME_API_TOKEN="..."
Also deliberately excluded by .chezmoiignore: ~/.claude/.credentials.json, settings.local.json (per-machine permission grants), and every Claude Code runtime directory (projects/, sessions/, tasks/, plugins/, history.jsonl). Those are machine state, not configuration — they’d conflict on every sync. If a real secret ever needs to live in the repo, the escape hatch is chezmoi add --encrypt with age.
Gotchas
~/.claudeis no longer its own git repo. It was previously tracked atgithub.com/stevecrawshaw/.claude, now archived (read-only) with the full pre-chezmoi history. This repo is the only source of truth — editing a skill now needsczrlike any other managed file.alias bash="code ~/bash.sh"had to go. It shadowed thebashbinary, which is the kind of footgun you only notice at the worst moment.R_HOMEcosts ~0.5s per interactive shell because the env script invokes R to discover it. That’s the tradeoff for never hardcoding a versioned path; the env file documents how to hardcode it if startup drags.chezmoi initregenerates~/.config/chezmoi/chezmoi.toml. Never hand-edit that file — edit.chezmoi.toml.tmplinstead.- The old Git-for-Windows alias file is stubbed out (original kept as
.pre-chezmoi.bakbeside it). A Git upgrade may restore the stock version — harmless, since~/.bashrcis sourced afterwards and wins.
Learnings
The migration paid off in the first month, mostly in ways I didn’t predict:
- Templates earn their keep at the edges. 90% of the config is identical on both machines; the templating handles the 10% that isn’t (paths, autocrlf, credential helpers) without forking the files.
- Copy-not-symlink removes an entire class of Windows problems. I never once thought about symlink privileges after switching. That alone justified chezmoi over the alternatives.
- Provisioning as code makes “nuke and reinstall” a legitimate strategy. The bootstrap script is idempotent and re-runnable, so the AI-testing laptop can be rebuilt without fear — which is exactly the property you want on a machine whose whole job is running experimental software.
- AI tooling is the most valuable thing in the repo. The Claude Code skills and agents took months to accumulate. Version-controlling them was the real motivation, and the shell config was the vehicle.
If you’re running more than one machine — and especially if one of them exists to break things on purpose — a chezmoi repo is worth the afternoon it takes to set up. The czu ritual is now the single best habit in my tooling.