Repository navigation
Migrates from Dotbot to Chezmoi for multi-host multi user configurations. - #8
Merged
Merged
Conversation
Adds the chezmoi source root and the data model, with nothing ported yet:
.chezmoiroot -> home/, keeping docs and CI out of the
dotfile namespace
home/.chezmoi.toml.tmpl prompts for host slug and git identity, which
land in each host's local config and are
never committed
home/.chezmoidata/hosts.toml known machines by opaque slug, recording
capabilities rather than identities
Host slugs are explicit rather than detected: this Mac's short hostname is
rewritten by macOS on network changes, so it cannot key anything.
Prompt strings are short and stable because --promptString matches on prompt
text, making them the non-interactive interface CI will use.
Dotbot remains the live installer; nothing on this host is affected yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zshrc becomes a pure loader over two directories: conf.d, managed here and overwritten on apply, and local.d, managed by nothing and loaded last so a host can override without forking the repo. Three per-host differences that were previously commented-out blocks swapped by hand are now real conditionals: the Homebrew PATH entry, the brew alias, and the Godot binary path. The other two are gone rather than templated. Both existed because zsh on WSL is exec'd from bashrc and so is not a login shell, meaning zprofile never ran there and nvm silently failed to load. Moving nvm and PATH into conf.d, which zshrc reaches on every host, removes the need for either. PATH has to come from zshrc on macOS in any case, since /etc/zprofile runs path_helper afterwards and reorders anything set earlier. zprofile is therefore not carried forward. The INSTALL_TOOLS mechanism is dropped. conf.d now does environment and aliases only; install actions move to chezmoi scripts in a later stage. Worth noting that its test was `[[ INSTALL_TOOLS = 'true' ]]` with no `$`, comparing a literal string to "true" and always failing, so `brew bundle` had not run from that path in some time. Also removed: a JAVA_HOME block whose only statement was commented out, and the dotfiles-install alias, which checked out per-host branches that no longer exist. Dotbot remains the live installer; nothing on this host is affected yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Identity is now rendered per user from that user's own local chezmoi config,
which is the mechanism that lets two admin users on one machine share this repo
while keeping separate credentials.
The credential helper becomes a real conditional rather than another
commented-out Cerberus-PC block: osxkeychain on macOS, store elsewhere. The
store path is git's default ~/.git-credentials rather than ~/.git-creds/<user>,
which needed a directory created first; $HOME already makes it per-user.
ssh gains an `Include ~/.ssh/config.d/*` seam. It sits at the top because ssh
takes the first value it finds for each keyword, so drop-ins have to precede the
defaults to be able to override them. IdentityFile is the exception, as those
accumulate rather than override, which is what a second identity wants. This
also replaces zsh/tools/github.zsh, which appended the same block to
~/.ssh/config on every single shell start.
Fixes branch-prune, which had never worked: `git branch --vv` is not a valid
option, only `-vv` is, and `awk '{print &1}'` should be `$1`. Verified against a
scratch repository with a genuinely gone branch.
Not carried forward: the ~/DevProjects/ includeIf and gitconfigs/git-personal.conf.
Both set the identity to exactly what the base config now renders from the
prompts, so they were duplication.
Note for cutover: git reads ~/.config/git/config only when ~/.gitconfig does not
exist, so the old Dotbot symlink must be removed or this file is ignored in
silence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One run_onchange script branching on host class: brew bundle on macOS, apt elsewhere. It is an `after` script rather than `before` because the macOS path reads ~/.Brewfile, which chezmoi has not written during the before phase. The Brewfile is templated with casks gated on the host's gui capability, so a headless host renders the file without that section rather than carrying commented-out lines. On non-macOS hosts .chezmoiignore drops the file entirely. The apt path works out which packages are actually missing before invoking sudo, so an already provisioned host never triggers a password prompt during an apply. The brew path does not attempt to reproduce the interactive `brew` alias that proxies through a dedicated homebrew user. That alias is a shell alias and is not available here; sudo would need a password and -H would reset $HOME, which would make --global read the wrong Brewfile. Running as the invoking user works because the Cellar is group-writable by admin. Failure is deliberately non-zero. chezmoi records a run_onchange script as run whenever it exits 0, including when it exits early having done nothing, so exiting 0 on failure would mean never retrying. On the first run against a machine set up by hand, brew bundle is expected to fail on casks whose apps already exist in /Applications; the script prints the --adopt invocation that resolves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Submodules were a poor fit. They pin a commit that has to be bumped by hand, they need `git submodule update --init` before anything works, and when that step is missed the failure surfaces as a confusing "no such file or directory" at shell startup rather than as anything actionable. This repository was in exactly that state. Neither submodule had ever been initialised, so $ZSH/oh-my-zsh.sh did not exist and every shell start printed an error while the framework silently failed to load. Verified that it now loads: the omz function, the git plugin's aliases and the theme are all present in a shell booted against a fresh apply. oh-my-zsh's own auto-updater is disabled, since chezmoi now owns the checkout and refreshes it weekly. Leaving both enabled would mean two things pulling the same repository, and its interactive update prompt appearing in new shells. The dotbot submodule stays for now: ./install re-initialises it, so removing it would break the live installer while it is still the thing configuring this machine. It goes in Stage 11 along with the rest of Dotbot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The assertions live in scripts/render-check.sh rather than in the workflow, so they run identically on a laptop and in CI. CI that cannot be reproduced locally gets ignored, and the workflow is a thin wrapper around the script. It renders four host profiles, two of them synthetic so that classes no real host has yet are still covered, and asserts on the rendered output rather than on exit codes. That distinction matters: when a prompt goes unanswered non-interactively chezmoi substitutes the prompt text as the value and exits successfully, so a green build proves nothing on its own. One assertion exists purely to catch that. Verified by breaking things on purpose: casks leaking onto a headless profile, a zsh syntax error, a broken Go template in both a script and a dotfile, and a simulated prompt substitution are each caught. An unknown host slug now fails at a single choke point in .chezmoiignore, which is rendered on every apply, with a message naming the bad slug instead of the previous "nil data; no entry for key class". The forbidden-pattern check is a local pre-commit hook rather than a CI job. Logs from a public repository are public, so a CI check whose failure output named the matched string would publish the thing it guards; and CI only detects after a push, by which point it is already public. The pattern list is not in this repository — the script reads an untracked file and is a silent no-op when it is absent, which is the correct behaviour on a machine with nothing to protect. It never prints a pattern, a matching line, or a filename. gitleaks covers committed credentials, which is a different problem: it detects credential shapes and would not have caught the work email address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chezmoi does not persist --source on its own. Without this the generated config has no sourceDir and every later command falls back to ~/.local/share/chezmoi, which would leave two clones of this repo drifting apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
git reads both ~/.config/git/config and ~/.gitconfig, in that order, rather than ignoring the former when the latter exists. The older file therefore overrides every key it defines while keys unique to the newer file still apply. That partial shadowing is the more dangerous case: most settings work, so nothing looks broken, while specific ones quietly come from the stale file. Verified on git 2.55. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bash files under wsl/ had never been ported; wsl/ held the only copies, so Stage 11 would have deleted them. The shim is now templated and excluded on macOS, where bash is never the login shell. It is much smaller than what it replaces. Once bash execs zsh, none of the Debian default bashrc runs, so history sizes, ls colours, completion and the prompt are dropped rather than carried. Two guards are tightened: the handoff now happens only for interactive shells, where the previous was also true for non-interactive commands with a terminal attached and would break scp and ssh-with-a-command; and only when zsh is actually installed, so a half-provisioned host still gets a usable bash. The gcloud cask was renamed upstream to gcloud-cli. The fix had been applied to ~/.Brewfile and to the pre-migration Brewfile, but not to the template that generates the former, so the next apply would have reverted it. render-check now covers both: that the shim appears only on non-macOS hosts, and that rendered bash files parse. Verified the new check fails when it should. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deletes install, install.conf.yaml, the pre-migration Brewfile, zsh/, gitconfigs/, wsl/, submodules/ and .gitmodules. Nothing in the new tree referenced any of them, render-check still passes for all four host profiles, and this machine is unaffected. Kept scripts/autokey-github.sh: it uploads an SSH key to GitHub, has nothing to do with Dotbot, and is about to be useful. The README loses the Branch Structure section describing per-host branches that no longer exist, and gains install, day-to-day and adding-configuration sections. It states plainly that editing a file in $HOME no longer edits the repo, which is the one habit that does not survive the move from a symlink manager. Adds scripts/doctor.sh, which validates a live host the way render-check validates the repository: that the apply landed, that nothing from a previous setup is silently shadowing it, that identities resolve where they should, and that a fresh login shell is clean. It is how hosts I cannot log into get verified. Three game-related scripts under zsh/projects and zsh/apps are deleted without being ported. They were commented out of the source list and so had not been loading; they remain in history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The base config no longer sets user.name/user.email. Identity now comes from a directory-scoped includeIf, and user.useConfigOnly makes git refuse to commit in any tree that has no rule. This is what lets several macOS accounts share one public repo without becoming symmetric. Every account gets the same base — public identity, ~/DevProjects. An account that needs a second identity adds one host-local rule scoped to its own tree; the public repo neither knows nor can be made to reveal that any such tree exists. useConfigOnly is a leak guard rather than a style preference. Without it git synthesises an identity from $USER and the hostname, which on a secondary account puts that account's name in the author field of a public commit. The ~/WorkProjects/work/ includeIf is removed for the same reason: a tree that belongs to one account should be configured on that account, not announced here. The parked host-local file is untouched and simply inert until then. render-check and doctor now test identity functionally, against throwaway repos under a HOME pointed at the rendered tree. `git config --file` does not evaluate includeIf, so the previous textual assertion would have passed no matter what. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
A substantial re-write of my entire dot files configuration management solution.
See the README for reasoning for switching from Dotbot to Chezmoi. Maintains as much of the old functionality as possible.
Co-authored with Claude Opus.