Manage dotfiles and packages with chezmoi. Use when working with dotfiles, config files outside of project directories, chezmoi templates, brew/mise packages, machine-specific configuration, or when the user mentions chezmoi, dotfiles, packages, or configuration management. Also triggers when the user wants to install, remove, or update brew packages, casks, or mise tools.
This repo is a chezmoi dotfiles repository at ~/.local/share/chezmoi/.
See CLAUDE.md in the repo root for full architecture documentation.
.tmpl version exists, edit that — never the generated file.chezmoi add on a templated file — it strips template logic.
Copy directly to the source instead.chezmoi from within a chezmoi script — chezmoi holds a
lock during apply and it will deadlock.chezmoi diff or chezmoi apply --dry-run.Fish is the primary shell, but bash (dot_bashrc.mine.tmpl) and zsh
(dot_zshrc) configs must stay consistent for shared concerns:
/opt/homebrew/binplugins.sh sourced in all threemise activate in all threestarship init in bash and zshWhen adding something to fish config, check if bash/zsh need the equivalent.
When the user asks to configure something (a new app, shell setting, tool, environment variable, etc.), always put the configuration into chezmoi:
dot_config/ (or appropriate chezmoi path).chezmoidata/packages.yaml (not brew install)dot_config/mise/config.toml (not npm -g)dot_config/fish/config.fish.tmpl (not ~/.config/fish/config.fish).chezmoiscripts/ with proper run_once_/run_onchange_ prefix.chezmoiignoreThe goal: chezmoi apply on a fresh machine should fully reproduce the
user's environment. If a change wouldn't survive chezmoi apply, it's wrong.
Never run brew install, npm install -g, pip install, or similar commands
directly. Always update the declarative config and let chezmoi apply handle
installation.
| Actual path | Chezmoi source |
|---|---|
~/.foo | dot_foo |
~/.config/bar | dot_config/bar |
~/.ssh/config | private_dot_ssh/config |
| Sensitive files | private_ prefix (0600 permissions) |
| Executables | executable_ prefix |
| Templates | .tmpl suffix |
Source directory: ~/.local/share/chezmoi/
All variables defined in .chezmoi.toml.tmpl. Key flags:
.targetname # machine name (starship, nessie, work hostnames)
.is_work_machine # semantic boolean
.is_personal_machine # semantic boolean
.is_wife_machine # semantic boolean
.is_child_machine # semantic boolean
.is_laptop # semantic boolean
.personalpackages # true on non-work machines
.work.user, .work.domain # work identity
.packages.* # package lists from .chezmoidata/packages.yaml
Use chezmoi data to see all current values.
Use chezmoi execute-template '{{ .is_work_machine }}' to test snippets.
{{- trims leading whitespace, -}} trims trailing. Both are needed on
guards before shebangs to avoid blank lines:
{{- if (eq .chezmoi.os "darwin") -}}
#!/bin/bash
chezmoi source-path ~/.config/foo/bar.tmpl suffix in source~/.local/share/chezmoi/)chezmoi diffchezmoi apply (or chezmoi apply ~/.config/foo/bar for one file)chezmoi add ~/.config/foo/bar # Add as static file
chezmoi add --template ~/.config/foo # Add as template (if it needs variables)
{{ if .is_work_machine -}}
# work-only config
{{- end }}
{{ if .personalpackages -}}
# personal machine only
{{- end }}
For excluding entire files by machine type, add entries to .chezmoiignore.
Packages are declared in .chezmoidata/packages.yaml under .packages.brew.
The brew install script (run_onchange_01_install-brew-packages.sh.tmpl)
re-runs automatically whenever the rendered package list changes.
| YAML key | Description | Gated by |
|---|---|---|
packages.brew.taps | Homebrew taps | — |
packages.brew.brews | CLI tools (all machines) | — |
packages.brew.casks | GUI apps (all machines) | — |
packages.brew.personal_brews | CLI tools (personal only) | .personalpackages |
packages.brew.personal_casks | GUI apps (personal only) | .personalpackages |
.chezmoidata/packages.yamlchezmoi apply — the run_onchange_ script auto-triggers# Example: add a new CLI tool
# Edit .chezmoidata/packages.yaml, add under packages.brew.brews:
# - ripgrep
# Then:
chezmoi apply
Same as above but under packages.brew.casks or packages.brew.personal_casks.
nettleton/tap is hardcoded in the brew script (not in YAML) because it uses
a custom SSH git URL. Auth token is injected via op read. To add a formula
from this tap, add it as "nettleton/tap/formulaname" in the brews list.
Tool versions are in dot_config/mise/config.toml. The mise install script
(dot_config/mise/run_onchange_configure-mise.fish.tmpl) re-runs when the
config changes.
# dot_config/mise/config.toml
[tools]
java = "corretto-21"
python = "latest"
node = "latest"
pipx = "latest"
"npm:neovim" = "latest"
"npm:npm" = "latest"
To add a new tool or npm package, edit this file and run chezmoi apply.
Fisher plugins are listed in .chezmoidata/packages.yaml under
.packages.fisher. The fish config script installs them during
02-00_configure-fish.
Go packages are in .chezmoidata/packages.yaml under .packages.go.
The install script (run_onchange_03-02_install-go.fish.tmpl) re-runs
when the list changes.
MAS apps are in .chezmoidata/packages.yaml under .packages.mas.apps
(with name and id). The install script
(run_onchange_04-01_install-mas-apps.sh.tmpl) re-runs when the list changes.
When local drift exists (files modified outside chezmoi):
chezmoi status (shows drift between source and target)chezmoi diffchezmoi applyDo NOT use chezmoi add on templated files — copy directly instead:
# Wrong (strips template):
chezmoi add ~/.config/foo
# Right (preserves template):
cp ~/.config/foo ~/.local/share/chezmoi/dot_config/foo.tmpl
Scripts in .chezmoiscripts/ follow a numeric prefix taxonomy that controls
execution order (alphabetical by target path):
| Prefix | Phase | Purpose |
|---|---|---|
00 | Bootstrap | Install prerequisites (Homebrew, 1Password), system config (sudo, sshd, git origin, op plugins) |
01 | Brew packages | run_onchange_ — install taps, brews, casks from packages.yaml |
02 | Configure tools | run_once_ — configure tools installed by brew (fish, rust, go, mise, containers, git auth, etc.) |
03 | Other package managers | run_onchange_ — install packages via go, pip, etc. |
04 | Apps | Configure/install apps (MailMate, Mac App Store) |
05 | macOS defaults | System preferences (UI, energy, screen, Finder, Dock, app defaults, restarts) |
02-01_configure-rust only sets up Rust,
02-02_configure-go only sets up Go.02-06_configure-git-auth. Bad: 02-06_setup-stuff.run_onchange_ for package installs (re-run
when lists change), run_once_ for one-time configuration, run_after_
for every-apply fixups.All chezmoi scripts must be safe to run multiple times. chezmoi apply can
re-run at any time, and scripts must not fail or produce side effects on
repeated execution.
| Prefix | Re-runs when | Idempotency requirement |
|---|---|---|
run_once_ | Rendered content changes (hash-based) | Must handle already-configured state |
run_onchange_ | Rendered content changes (hash-based) | Must handle already-installed packages |
run_before_ | Every apply | Must be fast and safe to repeat |
run_after_ | Every apply | Must be fast and safe to repeat |
brew bundle is inherently idempotent (skips installed)grep -q before appending)if not test -d, command -v)mkdir -p, ln -sfn (safe to repeat)[ -f "$file" ] && chmod ...)These run on every chezmoi apply. Keep them fast and guard every operation:
#!/bin/bash
[ -f "$HOME/.netrc" ] && chmod 400 "$HOME/.netrc"
exit 0 # Always succeed (avoid blocking apply)
run_onchange_ scripts embed content hashes in comments to trigger re-runs
when data changes:
# packages hash: {{ include ".chezmoidata/packages.yaml" | sha256sum }}
# mise config hash: {{ include "dot_config/mise/config.toml" | sha256sum }}
When the referenced file changes, the rendered script content changes, and chezmoi re-runs it.
Scripts, externals, and files run alphabetically by target path during apply. This matters for dependencies:
dot_config/mise/config.toml (placed as .config/mise/config.toml)dot_config/mise/run_onchange_configure-mise.fish.tmpl (runs as
.config/mise/configure-mise.fish — sorts after config.toml)If a script depends on a config file, ensure the script's target path sorts after the config file's target path.
All secrets come from 1Password:
op read "op://vault/item/field" in scripts{{ onepasswordRead "op://vault/item/field" }} in templates| Mistake | Fix |
|---|---|
| Edit generated file instead of source | Always find source first with chezmoi source-path |
chezmoi add on a template | Copy directly to source |
Call chezmoi inside a chezmoi script | Use hardcoded paths instead |
| Package not in alphabetical order | Sort the YAML list |
Missing {{- before shebang | Add whitespace trimming to avoid blank lines |
| Script runs before its config is placed | Rename so target path sorts after config |