feat: migrate from dotbot to chezmoi#20
Merged
Merged
Conversation
vaintrub
added a commit
that referenced
this pull request
May 13, 2026
Review of PR #20 caught two real issues + one doc gap. M1 — set -u vs set -eu inconsistency install-packages was the lone holdout on `set -u` (no -e); every other script uses `set -eu`. Original rationale was "tolerate brew bundle failures" but every soft-failing command (brew bundle, apt-get install, dnf install, npm install -g) is already wrapped with `||` — `set -e` doesn't trip on those. Aligned to `set -eu` on both Mac and Linux branches. One caveat: brew bundle's exit status used to be captured via `brew_status=$?` after the heredoc — that pattern doesn't work with `set -e` (failure aborts before $? capture). Rewrote with `|| brew_status=$?` inline, which set -e treats as part of the conditional. Verified pattern with a minimal test. M2 — fnm install/default error visibility `fnm install --lts && fnm default lts/latest` silently no-ops if install fails (the && short-circuits before fnm default). With set -eu aligned, just split into two consecutive statements: install fails → script aborts with fnm's own error visible. Cleaner than `|| echo` chains. M8 — install-rtk error message detail "rtk not found on PATH after install-packages — check packages.yaml / brew state" was too generic. Common cause is brew bundle aborting because VSCode or Docker cask adoption needs sudo (non-interactive apply can't supply). Expanded to four lines: explain cause + actionable fix (rerun from TTY or `sudo -v` to cache credentials). R1 — Linux npm prefix doc note Reviewer concern: claude/codex npm globals might not be on PATH for 70-install-plugins on a fresh box. Investigated: with default npm prefix (`/usr`) binaries land at `/usr/bin/{claude,codex}` — already on PATH for any child sh. Risk only with non-standard prefix (`npm config set prefix ~/.local`). No code change needed — added troubleshooting paragraph to README Linux section. Deferred (out of scope, per plan): - L5 splitn _0/_1 keys (sprig-idiomatic, accept) - L6 DRY headless detection (3 instances, marginal win) - L7 .chezmoiremove.tmpl empty placeholder (twpayne pattern) - R2 VSCode adoption sudo prompt (pre-existing limitation) - R3 Caveman plugin restart (Claude prints "Restart" itself) - R4 CI checks (substantial work, separate task)
2e96144 to
125c4f8
Compare
Replaces the prior dotbot + ad-hoc shell-install setup with a single
chezmoi source tree at ~/.local/share/chezmoi (XDG default — no custom
sourceDir override). Identical cross-platform model for macOS + Linux
(Debian/Ubuntu/Fedora), bootstrapped via the canonical one-liner:
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply vaintrub
Highlights:
- 3-tier install profile, use-case-driven (`core` / `dev` / `workstation`).
OS is detected at runtime under the hood; profile picks USE CASE, not
platform:
core minimum to make dotfiles functional. Hetzner VPS,
DO droplet, jetson SSH first-touch, Codespace, recovery.
~50 MB.
dev core + CLI dev toolchain (mise tools + Linux apt/dnf
extras + Mac brews — symmetric, modulo Docker engine
which is `workstation`-only on Mac via Docker Desktop).
Headless dev box.
workstation dev + GUI apps. Mac: casks (iterm2, docker-desktop,
vscode, ngrok, fonts). Linux: empty placeholder for
when a Linux desktop machine joins the fleet.
Profile is ALWAYS prompted at `chezmoi init`; environment is shown
as a hint in the prompt text (detected env + SSH + GUI flags); user
picks explicitly. Default = `core` (safe fallback for CI / non-TTY).
- mise as primary cross-platform dev-tool installer. Single declarative
`dot_config/mise/config.toml.tmpl` lists 25 aqua-backend tools
(kubectl, helm, k9s, kustomize, stern, argocd, opentofu, awscli,
rclone, jq, gh, fzf, fd, yq, delta, shellcheck, buf, golangci-lint,
goreleaser, gotestsum, protoc-gen-go, pnpm, uv, cloudflared,
websocat, gitleaks) + 4 language runtimes (node/go/python/rust).
Profile-aware: core renders just fzf+zoxide; dev/workstation render
the full set. ~600 LOC of per-tool bespoke installers (GitHub-release
downloads, arch shims, apt-repo wiring) gone in favour of mise's
baked-in registry. Mac brew + Linux apt/dnf handle only OS glue and
tools not in aqua (htop/tree/wget/nmap/telnet/libpq/wireguard-tools).
- Thin-wrapper / fat-library split for install scripts. The
`run_onchange_after_50-install-packages.sh.tmpl` wrapper renders
chezmoi facts (profile, osid, every package list) into DOTFILES_*
env vars and sources `lib/install-packages.sh` (pure POSIX shell,
no template syntax). Library is bats-testable: 25 unit tests cover
predicates (is_dev cascade, is_workstation, is_debian_family with
idLike fallback, is_fedora_family), apt/dnf/brew install paths
(root vs sudo, profile cascade, missing-sudo skip), dispatcher
branching, and the INSTALL_PACKAGES_INVOKE guard.
- Network safety baseline at core tier on Linux: `ufw` (disabled by
default; user opts in via `sudo ufw enable`) + `tcpdump`. Mac uses
the App Firewall (System Settings); tcpdump ships at /usr/sbin/.
- gitleaks pre-push hook wired globally via
`core.hooksPath = ~/.config/git/hooks` in `dot_gitconfig.tmpl`. Runs
`gitleaks git --log-opts=<remote..local>` on outbound commits; fails
open if gitleaks not on PATH (core-profile machines). Per-repo
opt-out: `git config --local core.hooksPath ''`.
- Touch ID for sudo on Mac via /etc/pam.d/sudo_local (Sonoma+ Apple-
blessed extension point that survives system updates). Pre-Sonoma
fallback edits /etc/pam.d/sudo directly. Idempotent grep guards;
TTY check skips on non-interactive applies.
- OSC 133 semantic prompt sequences emitted from zshrc (preexec/precmd
hooks). Powers shell-integration features in iTerm2 (⌘↑/⌘↓ jump),
VS Code (command decorations), Wezterm (ScrollToPrompt), Windows
Terminal, Ghostty. Skipped when p10k Pro or iTerm2 shell-integration
is already emitting the marks.
- alker0/chezmoi.vim plugin in dot_vimrc — auto-detects chezmoi source
files (dot_*, *.tmpl, modify_*, .chezmoitemplates/*), strips
prefixes to derive the real filetype, and registers composite
`<lang>.chezmoitmpl` syntax for Go-template-aware highlight. Honors
.chezmoiroot, intercepts /tmp/chezmoi-edit* hardlinks.
- Hooks-based prereq install via .chezmoi.toml.tmpl
`[hooks.read-source-state.pre]` — apt/dnf installs git/zsh/vim/
tmux/curl/ca-certificates if missing (cert-bundle file check, not
binary check), then curl-pipes mise into ~/.local/bin. Idempotent,
silent on clean systems (stdout suppressed), skips with a clear
warning if non-interactive without passwordless sudo.
- modify_ scripts for Claude settings.json (jq additive merge) and
Codex config.toml (chezmoi-native fromToml/toToml merge). Both
preserve tool-mutated keys (rtk init hooks, plugin enable lists)
across applies. Bases live in .chezmoitemplates/ and load via
includeTemplate.
- Single source of truth for plugins in .chezmoidata/packages.yaml:
.plugins.{claude, claude_marketplaces, codex, codex_marketplaces}
drives both the install loop in 70-install-plugins and the enable
flags inside .chezmoitemplates/{claude-settings-base.json,
codex-config-base.toml}.
- Externals replace git submodules: gpakosz/.tmux pinned by commit
SHA in .chezmoiexternal.toml.tmpl; MesloLGS NF + Monaspace Neon
fonts pulled into ~/Library/Fonts / ~/.local/share/fonts
(Monaspace version hoisted to a single template var; Monaspace zip
extracted via archive-file external for 4 canonical weights).
Headless-SSH boxes skip font install.
- iTerm2 prefs live at iterm/com.googlecode.iterm2.plist (chezmoi-
ignored); iTerm2 reads + writes there directly via
PrefsCustomFolder, set on first apply by a Mac run_once_after_*
script. No symlink layer.
- Per-machine override file via `create_dot_zshrc_local` (chezmoi-
native seed-once attribute; sourced LAST by dot_zshrc so local
edits win over everything tracked).
- .osid composite key derived once in .chezmoi.toml.tmpl from OS +
osRelease.id (darwin / linux-ubuntu / linux-debian / linux-fedora /
...). Replaces nested if-elif distro dispatch; library predicates
`is_debian_family` / `is_fedora_family` (with idLike fallback for
Pop!_OS / Mint / Raspbian) consume it.
- GitHub Actions CI: `unit` job (bats tests/unit/) → `apply-core`
job (chezmoi apply in devcontainer-style ubuntu-latest + bats
tests/files/). Triggers on push (master, chore/**, feat/**, fix/**,
docs/**) + PRs. Symlinks $GITHUB_WORKSPACE → ~/.local/share/chezmoi
pre-init so subsequent chezmoi invocations find source.
- Repo docs: README.md (user-facing install + profile guide) +
AGENTS.md (auto-loaded by Claude/Codex when working in the repo,
CLAUDE.md is a symlink). Per-area rules under dot_claude/rules/
auto-load at session start (frontend rules conditional via
`paths:` glob).
Migration cleanup: deleted .dotbot, .install.conf.yaml, install,
.gitmodules, tmux/oh-my-tmux submodule, claude/+codex/ flat dirs.
Renamed top-level dotfiles to chezmoi dot_ source-state names.
Legacy `profile = "mac"` from earlier core/dev/mac tiering silently
translates to `workstation` in the template — existing Mac users
keep their GUI install across the upgrade.
b1cea83 to
057c640
Compare
Open PR #20 + push to branches matching the push trigger globs caused GitHub Actions to fire two identical runs on every commit — one labeled with the PR title, one with the latest commit message — both targeting the same SHA. Wasted runner minutes + cluttered run list. Solo repo with no fork PRs expected: `push` event alone covers master + every working branch (chore/**, feat/**, fix/**, docs/**), and the PR view surfaces the latest push's CI status automatically. Re-enable `pull_request:` if external contributors start opening PRs from forks — push trigger doesn't fire on the fork's source branch since those commits don't land in this repo until merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrate dotfiles management from dotbot to chezmoi, then iterate to a canonical-chezmoi shape: single
.chezmoidata/packages.yamlSoT for packages + AI plugins, canonical default sourceDir (~/.local/share/chezmoi/), pre-source-state hook for prereq install, opentofu, structured plugin install with caveman as a regular plugin (no bespoke installer).46 commits on
chore/chezmoi-migrationagainstmaster.Why
fromToml/mergeOverwritefor Codex config.toml) handles tool-mutated files cleanly — preserves rtk hooks + plugin enables across applies.chezmoiexternal.toml.tmplwith SHA-pinned commit replaces git submodule for oh-my-tmux; same mechanism downloads MesloLGS NF fonts cross-platformsh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply vaintrub(no install.sh, no--sourceflag, no override)packages.yamlas single SoT drives both install scripts AND.chezmoitemplates/{claude-settings-base.json,codex-config-base.toml}plugin enable flags — no install/enable driftclaude plugin install caveman@caveman(per upstream INSTALL.md per-agent table). Plugin self-registers SessionStart + UserPromptSubmit hooks via plugin.json. Previous bespokebash install.shwas double-wiring hooks into settings.json → duplicate firings. Fixed.hooks.read-source-state.pre.commandrunshooks/ensure-prereqs.shBEFORE chezmoi reads source state — installsgit/zsh/vim/tmux/curlvia apt/dnf on Linux or Xcode CLT + Homebrew on Mac. True zero-touch bootstrap on supported distros (Debian/Ubuntu/Fedora, macOS).Architecture
dot_Xmodify_X.tmpl+ jq~/.claude/settings.jsonmodify_X+#chezmoi:modify-template~/.codex/config.tomlsymlink_X.chezmoiexternal.toml.tmpl.chezmoidata/packages.yaml.chezmoitemplates/{X-base}.chezmoiscripts/{50-,60-,70-}.chezmoiscripts/{darwin,linux}/hooks/ensure-prereqs.sh[hooks].read-source-state.preKey state
~/.local/share/chezmoi/(XDG_DATA_HOME compliant). No override. Optional~/dotfilessymlink for muscle memory (not required)..chezmoi.toml.tmpl:stdinIsATTY+hasKey+ generic placeholders pattern (mkasberg/chezmoi-official idiom). Safe for forkers, Codespaces force non-interactive.~/.codex/skills/dir with file-level symlink to Claude'ssave-to-dotfilesskill. No cross-leak.Verification (this machine, post-apply)
chezmoi diffchezmoi doctor~/.claude/CLAUDE.mdreal file with@RTK.mdref~/.claude/settings.jsonjq-merged: base + rtk PreToolUse hook + enabledPlugins~/.codex/config.tomlclean, base + runtime sections stripped, idempotentclaude plugin listPrefsCustomFolder/Users/vaintrub/.local/share/chezmoi/iterm(canonical)~/Library/Fonts/~/.codex/skills/save-to-dotfiles/SKILL.md~/.config/tmux/Out of scope (TODO in README)
dot_gitconfig.tmplconsuming the name/email promptsdot_ssh/config.tmplcross-platform SSH defaultsdot_kube/,dot_aws/cloud configs