|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Migrating my dotfiles to chezmoi: one config for a work desktop and an AI-harness-testing laptop" |
| 4 | +date: 2026-08-18 |
| 5 | +categories: [Development Tools] |
| 6 | +tags: |
| 7 | +- chezmoi |
| 8 | +- dotfiles |
| 9 | +- ubuntu |
| 10 | +- windows |
| 11 | +- claude |
| 12 | +- ai-coding |
| 13 | +- shell |
| 14 | +- dev-tools |
| 15 | +--- |
| 16 | + |
| 17 | +**TLDR; I migrated my dotfiles into a single [chezmoi](https://chezmoi.io)-managed repo at [github.com/stevecrawshaw/dotfiles](https://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.** |
| 18 | + |
| 19 | +## Why I needed this |
| 20 | + |
| 21 | +For years my configuration lived in roughly three places, none of them authoritative: |
| 22 | + |
| 23 | +- a `.bashrc` for the work desktop, hand-edited over several jobs' worth of cruft |
| 24 | +- a `.zshrc` on the new Ubuntu laptop, written from scratch and already diverging |
| 25 | +- `~/.claude` as 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 |
| 26 | + |
| 27 | +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. |
| 28 | + |
| 29 | +## The laptop's job: an AI harness testbed |
| 30 | + |
| 31 | +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. |
| 32 | + |
| 33 | +That role drives three decisions in the dotfiles: |
| 34 | + |
| 35 | +1. **The entire `~/.claude` tree 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. |
| 36 | +2. **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. |
| 37 | +3. **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. |
| 38 | + |
| 39 | +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. |
| 40 | + |
| 41 | +## Why chezmoi |
| 42 | + |
| 43 | +I considered the usual options first: |
| 44 | + |
| 45 | +- **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. |
| 46 | +- **GNU Stow** — same symlink problem, and no templating. |
| 47 | +- **Dotbot or a custom script** — reinventing the wheel, badly. |
| 48 | + |
| 49 | +chezmoi won on three concrete points: |
| 50 | + |
| 51 | +1. **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. |
| 52 | +2. **Go templates.** One source file can render differently per machine — the gitconfig's `core.editor` path and `autocrlf` setting, 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. |
| 53 | +3. **`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. |
| 54 | + |
| 55 | +## The repo layout |
| 56 | + |
| 57 | +```text |
| 58 | +.chezmoi.toml.tmpl generates per-machine config on `chezmoi init` |
| 59 | +.chezmoiignore per-OS exclusions (target paths, not source names) |
| 60 | +.chezmoitemplates/ shared content included by several targets |
| 61 | + vscode-settings.json the real VS Code settings |
| 62 | + positron-settings.json the real Positron settings |
| 63 | +dot_bashrc.tmpl -> ~/.bashrc (Windows) |
| 64 | +dot_zshrc.tmpl -> ~/.zshrc (Ubuntu) |
| 65 | +dot_gitconfig.tmpl -> ~/.gitconfig |
| 66 | +dot_config/shell/aliases.sh.tmpl -> ~/.config/shell/aliases.sh (both shells) |
| 67 | +dot_config/shell/env.sh.tmpl -> ~/.config/shell/env.sh (both shells) |
| 68 | +dot_config/ghostty/config -> ~/.config/ghostty/config (Ubuntu) |
| 69 | +dot_claude/ -> ~/.claude (Claude Code config) |
| 70 | +AppData/Roaming/Code|Positron/ -> editor config (Windows) |
| 71 | +dot_config/Code|Positron/ -> editor config (Ubuntu) |
| 72 | +*-extensions.txt extension manifests, replayed by run_onchange_ |
| 73 | +run_onchange_* provisioning scripts |
| 74 | +``` |
| 75 | + |
| 76 | +A few things worth calling out: |
| 77 | + |
| 78 | +- **The `dot_` prefix is chezmoi's convention** — a source file called `dot_zshrc.tmpl` becomes `~/.zshrc`. |
| 79 | +- **Editor settings live in exactly one place.** VS Code reads from `%APPDATA%\Code\User` on Windows and `~/.config/Code/User` on Linux, so both trees exist in the repo — but each is a one-line file that includes the same shared template from `.chezmoitemplates/`. `.chezmoiignore` excludes the irrelevant tree per OS. |
| 80 | +- **What's templated is only what actually diverges** — aliases (OneDrive paths, `start msedge`, winpty wrappers are work-only), env (R/rv paths), gitconfig (`autocrlf` `false` vs `input`, the gh credential helper on Linux only). VS Code keys with `.windows` / `.linux` suffixes are left untemplated because VS Code selects those at runtime. |
| 81 | +- **`~/.claude` is the biggest subtree** — ten agents, thirty-plus skills, hooks, commands and rules. That's the AI-testing payload riding along with the shell config. |
| 82 | + |
| 83 | +## Bootstrapping the Ubuntu laptop |
| 84 | + |
| 85 | +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: |
| 86 | + |
| 87 | +```bash |
| 88 | +sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" |
| 89 | +export PATH="$HOME/.local/bin:$PATH" |
| 90 | +chezmoi init --apply --source="$HOME/projects/dotfiles" https://github.com/stevecrawshaw/dotfiles.git |
| 91 | +``` |
| 92 | + |
| 93 | +`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: |
| 94 | + |
| 95 | +- apt packages — zsh, git, fzf, ripgrep, build tooling, R, `gh` (from GitHub's own apt repo, because Ubuntu's build lags) |
| 96 | +- `uv`, radian (the R console VS Code uses), oh-my-posh, and the JetBrainsMono Nerd Font the prompt theme needs |
| 97 | +- chezmoi itself, plus reminders to run `gh auth login` and `chsh -s zsh` |
| 98 | + |
| 99 | +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. |
| 100 | + |
| 101 | +## Daily workflow |
| 102 | + |
| 103 | +The aliases make the loop short enough to be habit: |
| 104 | + |
| 105 | +```bash |
| 106 | +czd # chezmoi diff -- what would change on this machine |
| 107 | +cza # chezmoi apply -- write source -> $HOME |
| 108 | +czu # chezmoi update -- git pull + apply. The everyday sync. |
| 109 | +czs # chezmoi status |
| 110 | +czr # chezmoi re-add -- pull edits you made directly in $HOME back to source |
| 111 | +czcd # cd to this repo |
| 112 | +aka # edit the shared aliases file |
| 113 | +``` |
| 114 | + |
| 115 | +The typical loop after changing something: |
| 116 | + |
| 117 | +```bash |
| 118 | +czr && czcd && git add -A && git commit -m "feat: ..." && git push |
| 119 | +# then on the other machine |
| 120 | +czu |
| 121 | +``` |
| 122 | + |
| 123 | +<div class="callout warning"> |
| 124 | +**The one rule:** do not edit a managed file in `$HOME` and then run `cza` — apply overwrites `$HOME` from source. Run `czr` first to capture the edit. `czd` tells you which direction you're out of sync in. |
| 125 | +</div> |
| 126 | + |
| 127 | +`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. |
| 128 | + |
| 129 | +## Secrets |
| 130 | + |
| 131 | +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: |
| 132 | + |
| 133 | +```bash |
| 134 | +# ~/.env.local |
| 135 | +export SOME_API_TOKEN="..." |
| 136 | +``` |
| 137 | + |
| 138 | +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. |
| 139 | + |
| 140 | +## Gotchas |
| 141 | + |
| 142 | +- **`~/.claude` is no longer its own git repo.** It was previously tracked at `github.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 needs `czr` like any other managed file. |
| 143 | +- **`alias bash="code ~/bash.sh"` had to go.** It shadowed the `bash` binary, which is the kind of footgun you only notice at the worst moment. |
| 144 | +- **`R_HOME` costs ~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. |
| 145 | +- **`chezmoi init` regenerates `~/.config/chezmoi/chezmoi.toml`.** Never hand-edit that file — edit `.chezmoi.toml.tmpl` instead. |
| 146 | +- **The old Git-for-Windows alias file is stubbed out** (original kept as `.pre-chezmoi.bak` beside it). A Git upgrade may restore the stock version — harmless, since `~/.bashrc` is sourced afterwards and wins. |
| 147 | + |
| 148 | +## Learnings |
| 149 | + |
| 150 | +The migration paid off in the first month, mostly in ways I didn't predict: |
| 151 | + |
| 152 | +- **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. |
| 153 | +- **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. |
| 154 | +- **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. |
| 155 | +- **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. |
| 156 | + |
| 157 | +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. |
| 158 | + |
| 159 | +## Resources |
| 160 | + |
| 161 | +- [chezmoi](https://chezmoi.io) |
| 162 | +- [stevecrawshaw/dotfiles](https://github.com/stevecrawshaw/dotfiles) |
| 163 | +- [chezmoi quick start](https://www.chezmoi.io/quick-start/) |
0 commit comments