chezmoi
Wrap chezmoi — manage dotfiles across machines using source-state semantics, Go templates, and at-rest encryption.
When to use
- Bootstrapping dotfiles on a new machine
- Adding a new file to existing chezmoi-managed dotfiles
- Templating a dotfile per-OS, per-host, or per-role (work vs. personal)
- Adding secrets via 1Password / Bitwarden / age / gpg
- Writing a
run_once_install script - Diagnosing "chezmoi apply changed something I didn't expect"
- Designing the
.chezmoi.toml.tmplconfig template for a fleet of machines
Mental model
chezmoi maps a source directory (~/.local/share/chezmoi, a git repo) to
a target directory (the user's home). Source filenames carry attribute
prefixes that determine target name and behavior. Source files with a .tmpl suffix are rendered as Go text/template and
written to their target paths. Files under .chezmoitemplates/ are reusable
template fragments included by other templates — they do not produce target
files on their own. A
config file at ~/.config/chezmoi/chezmoi.toml (typically generated from
.chezmoi.toml.tmpl on first init) holds per-machine variables.
The two reasons people get into trouble:
- They edit files directly under
~/.local/share/chezmoi/instead of usingchezmoi edit $TARGET. This breaks templates and decrypt round-trips. - They commit secrets in plaintext "because the repo is private". Repos leak, forks leak, history is forever. Use a secret backend.
Where the config file lives
Two files, easy to confuse. Both can be in toml, yaml, json, or
jsonc (auto-detected by extension; two formats present at once is an
error, not "first one wins"):
| File | Path | Who writes it |
|---|---|---|
chezmoi.<fmt> (the config) |
$XDG_CONFIG_HOME/chezmoi/chezmoi.<fmt>, default ~/.config/chezmoi/chezmoi.<fmt> (.toml most common). NOT in the source repo. |
Generated by chezmoi init (or apply --init) from the template below. Per-machine. Holds secrets, API tokens, machine-specific settings — never commit. |
.chezmoi.<fmt>.tmpl (the config template) |
Source-state root: ~/.local/share/chezmoi/.chezmoi.<fmt>.tmpl (.toml most common). Committed to the dotfiles repo. |
You write it. Uses promptBoolOnce / promptChoiceOnce / promptStringOnce so a fresh machine generates the right config on first init. |
Override locations with --config <path> (use --config-format <fmt> if
the extension is unusual). Look up effective values with
chezmoi cat-config and chezmoi dump-config.
Protocol
1. Detect what the user wants
| User says | Go to |
|---|---|
| "set up chezmoi from scratch" / "new machine" | §2 Bootstrap |
| "add this file to my dotfiles" | §3 Add a file |
| "make this dotfile depend on OS / host" | references/templating.md |
| "encrypt this" / "store this token" | references/secrets.md |
| "run a script on first apply" | references/scripts.md |
| "chezmoi did something weird" | references/pitfalls.md |
| "design my full setup" | references/bootstrap.md |
2. Bootstrap (new machine)
The full one-liner that clones, renders templates, and applies in one shot:
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply $GITHUB_USERNAME
For a fully fleshed .chezmoi.toml.tmpl recipe with promptBoolOnce for
work-vs-personal-vs-headless, see references/bootstrap.md.
3. Add a file
chezmoi add ~/.zshrc # plain copy
chezmoi add --template ~/.gitconfig # add as a template (.tmpl suffix added)
chezmoi add --encrypt ~/.ssh/config # encrypted at rest
chezmoi chattr +template ~/.zshrc # turn an existing managed file into a template
chezmoi edit ~/.zshrc # edit the SOURCE — handles decrypt + .tmpl correctly
Then the safe-apply ritual (§5).
4. File-naming reference
The table users get wrong most often. Prefixes must appear in a specific
order, and the allowed prefix set depends on the target type —
/reference/source-state-attributes/ is the canonical source.
| Prefix | Effect on target |
|---|---|
dot_ |
Add leading . (dot_zshrc → ~/.zshrc) |
private_ |
Strip group + world permissions (mode 0600 / 0700) |
readonly_ |
Strip all write permission bits |
executable_ |
Add executable bit |
empty_ |
Allow empty file (default would remove it) |
encrypted_ |
Decrypt source via age/gpg before writing target |
symlink_ |
Create a symlink instead of a regular file (file form, target is symlink target) |
exact_ (dirs) |
Delete anything in target dir not present in source |
create_ |
Create target only if missing — never overwrite |
modify_ |
Treat source as a script (or template, see below) that produces target contents |
run_ |
Treat source as a script to execute (not a target file) |
before_ / after_ |
Order scripts relative to applying files |
once_ (scripts) |
Run only when contents have not been run successfully before |
onchange_ (scripts) |
Run only when contents change for THIS filename |
remove_ |
Remove the target if it exists |
external_ (dirs) |
Mark a directory as an external — pairs with .chezmoiexternal.<fmt> / .chezmoiexternals/; chezmoi ignores attributes on child entries |
literal_ (anywhere) |
Stop parsing further attributes from this point |
Allowed prefix order per target type (the order is rigid; rearranging is a parse error):
| Target type | Allowed prefixes (in order) | Optional suffix |
|---|---|---|
| Directory | remove_, external_, exact_, private_, readonly_, dot_ |
— |
| Regular file | encrypted_, private_, readonly_, empty_, executable_, dot_ |
.tmpl |
| Create file | create_, encrypted_, private_, readonly_, empty_, executable_, dot_ |
.tmpl |
| Modify file | modify_, encrypted_, private_, readonly_, executable_, dot_ |
.tmpl |
| Remove file | remove_, dot_ |
— |
| Script | run_, once_ or onchange_, before_ or after_ |
.tmpl |
| Symlink | symlink_, dot_ |
.tmpl |
Suffix .tmpl makes the file a template. .age / .asc are stripped
automatically when the corresponding encryption is configured.
Modify file detail. A modify_ source is a script by default — it reads
the current target on stdin and writes the new target to stdout. If the
file contains the string chezmoi:modify-template, lines containing that
marker are stripped and the rest is interpreted as a template; the existing
target is exposed in .chezmoi.stdin. Use the template form when you want
to surgically edit a structured file (JSON, TOML, YAML); use the script
form when you need a real interpreter (jq, awk, sed).
5. Safe-apply ritual
Before any apply that touches important files, run the inspection trio:
chezmoi status # one line per changed file
chezmoi diff # unified diff of pending changes
chezmoi apply --dry-run -v # full plan, including scripts that would run
chezmoi apply # only after the above looks right
For routine pulls on a machine you trust:
chezmoi update # = git pull --autostash --rebase + apply
chezmoi git pull -- --autostash --rebase && chezmoi diff # preview only — no apply
6. Edit safely
chezmoi edit ~/.zshrc # opens source in $EDITOR, handles .tmpl + encryption
chezmoi cd # subshell in the source directory (for git operations)
chezmoi re-add # refresh source from current target after editing target
chezmoi merge ~/.zshrc # 3-way merge target ↔ source ↔ destination
chezmoi merge-all # 3-way merge every file whose actual ≠ target (add --init to re-render config first)
Editor resolution: edit.command → $VISUAL → $EDITOR → vi (or
notepad.exe on Windows). chezmoi prints a warning if the editor returns
in < edit.minDuration (default 1s) — that catches the "I opened the
file but didn't actually edit it" case. Set to 0 to silence.
Never open ~/.local/share/chezmoi/encrypted_dot_ssh/... or *.tmpl files
directly in your editor — the file you see isn't the file chezmoi sees.
7. Application order (mental model)
chezmoi apply is deterministic. Each run does:
- Read source state, read destination state, compute target state.
- Run
run_before_scripts in ASCII order of target name. - Update entries in ASCII order of target name. Directories (including directories materialized by externals) are written before the files they contain. Externals are fetched/expanded during this phase.
- Run
run_after_scripts in ASCII order.
Implications:
- A
run_before_script must not read a file that an external will provide — externals are applied in phase 3, not before. - Target names are compared with all attribute prefixes stripped. Given
create_alphaandmodify_dot_beta,.betais updated beforealphabecause.beta<alphain ASCII. - chezmoi assumes source and destination don't change during a run. A
run_before_script that writes into either violates that assumption — behavior is undefined.
8. Partial-file management
When chezmoi shouldn't own the whole file, pick a pattern based on who the file belongs to:
modify_ — user owns the file, chezmoi stamps a block. The source
reads the existing target on stdin and writes the new target on stdout.
Script form, modify_dot_zshrc:
#!/usr/bin/env bash
set -euo pipefail
# Strip any prior chezmoi block from the existing file …
awk '/# chezmoi:begin/{s=1;next} /# chezmoi:end/{s=0;next} !s'
# … then append the current managed block.
cat <<'EOF'
# chezmoi:begin
export EDITOR=nvim
alias g=git
# chezmoi:end
EOF
Template form (for structured files), modify_dot_config_private_app.yaml.tmpl:
{{- /* chezmoi:modify-template */ -}}
{{- $cfg := dict -}}
{{- if .chezmoi.stdin -}}{{- $cfg = .chezmoi.stdin | fromYaml -}}{{- end -}}
{{- $_ := set $cfg "telemetry" false -}}
{{- $_ := set $cfg "user" (dict "email" .email) -}}
{{ toYaml $cfg }}
The if .chezmoi.stdin guard handles the initial-apply case where the
target doesn't exist yet. Use fromJsonc / fromToml / fromYaml +
toPrettyJson / toToml / toYaml for the round-trip.
.chezmoitemplates/ — chezmoi owns the file, fragments are shared.
Reusable blocks live under .chezmoitemplates/, pulled into regular
templates with {{ template "name" . }} (or includeTemplate "name" .
when you need to pipe the result through a function):
.chezmoitemplates/zsh-prompt # one file
dot_zshrc.tmpl # {{ template "zsh-prompt" . }}
dot_config/fish/config.fish.tmpl # {{ template "zsh-prompt" . }} too
Choose the pattern by ownership. modify_ when the user can freely
edit the file outside the managed block. .chezmoitemplates/ when
chezmoi renders the whole file but you want to DRY shared sections.
Mixing both in one target is usually a smell.
Hard rules
These are non-negotiable. Each one corresponds to a real foot-gun:
- Never commit plaintext secrets, even to a private repo. Repos leak,
get forked, get logged, get backed up. Pick a backend from
references/secrets.md. - Never edit encrypted or templated source files directly. Use
chezmoi edit $TARGETso the decrypt and template round-trips happen. - Always inspect before applying on shared/important files —
chezmoi difforchezmoi apply --dry-run -v.exact_on a directory deletes unmanaged entries; you want to see that coming. prompt*template functions belong only in the config-file template (.chezmoi.toml.tmpl). Using them in regular dotfile templates causes a prompt on every apply / diff / status, which defeats automation.
Secrets decision tree (summary)
Full details in references/secrets.md. Pick exactly one per repo.
- Public repo + cloud password manager → 1Password CLI / Bitwarden template functions. Secrets fetched at apply time, never on disk.
- Private repo + offline-friendly → age (
encrypted_prefix). Identity at~/.config/chezmoi/key.txt, lives outside the repo. - Sharing across a team → SOPS-encrypted
.chezmoidata/secrets.yaml. - Air-gapped / first-bootstrap → gitignored plaintext
.chezmoidata/local.yaml(transitional, not a long-term answer).
Mixing backends multiplies moving parts without making anything more secure.
Templating quick reference
Full details in references/templating.md.
{{- if eq .chezmoi.os "darwin" }}
export HOMEBREW_PREFIX="/opt/homebrew"
{{- else if eq .chezmoi.os "linux" }}
export PATH="/usr/local/bin:$PATH"
{{- end }}
{{- if and (eq .chezmoi.os "darwin") (lookPath "brew") }}
# brew is installed
{{- end }}
email = {{ .email | quote }}
Built-in template variables include .chezmoi.os, .chezmoi.arch,
.chezmoi.hostname, .chezmoi.username, .chezmoi.homeDir,
.chezmoi.sourceDir. User-defined data lives under [data] in
chezmoi.toml, or any .chezmoidata/*.{toml,yaml,json} file.
chezmoi runs templates with Go's missingkey=error option, so a typo
like {{ .gitub.user }} fails the apply instead of rendering empty.
Override via [template] options = ["missingkey=zero"] only if you have
a concrete reason — losing the typo guard is rarely worth it.
For symlink targets, leading/trailing whitespace in the rendered result
is stripped, and an empty result removes the symlink. For file targets,
an empty render removes the file (use the empty_ prefix to keep it).
Special files and directories
All entries in the source dir beginning with . are ignored except the
specials below. They are evaluated in this order on every operation:
.chezmoiroot → .chezmoi.<fmt>.tmpl → data files → .chezmoitemplates/
→ .chezmoiignore → .chezmoiremove → externals → .chezmoiversion.
| Path | Role |
|---|---|
.chezmoiroot |
Single-line file pointing at a sub-directory as the source root |
.chezmoi.<fmt>.tmpl |
Config-file template used on first chezmoi init (and on --init) |
.chezmoidata.<fmt> / .chezmoidata/ |
Data merged into the template . namespace before any template renders |
.chezmoitemplates/ |
Reusable template fragments ({{ template "name" . }}) — no target files |
.chezmoiscripts/ |
Scripts not bound to a target file; honor run_before_ / run_after_ |
.chezmoiignore |
Per-target ignore list — source-relative, templatable |
.chezmoiremove |
Targets to remove on apply — templatable |
.chezmoiexternal.<fmt> / .chezmoiexternals/ |
Pull files/archives from URLs at apply time |
.chezmoiversion |
Min chezmoi version required by this source repo |
.chezmoiignore matches source-relative paths (e.g. dot_zshrc), not
target paths. People get this wrong constantly.
<fmt> means toml, yaml, json, or jsonc. The directory forms
(.chezmoidata/, .chezmoiexternals/) merge their contents in lexical
order with any same-prefix file forms — useful for splitting big config
across files or composing externals per role.
What you don't do
- Write a competing dotfile manager or shim around
chezmoi apply - Edit files under
~/.local/share/chezmoi/directly when they are templates or encrypted - Commit a
.env/secrets.yamlto a "private" repo - Suggest
chezmoi applywithout first runningchezmoi diffon a shared machine - Use
prompt*template functions outside of.chezmoi.toml.tmpl - Mix multiple secret backends (age + 1Password + SOPS) in the same repo without a clear, written reason
Gotchas
fork/exec ...: exec format errorrunning a templated script means a newline before#!. Add-inside the closing}}on the first template line to suppress it.fork/exec ...: permission deniedfor scripts means$TMPDIRis mountednoexec. SetscriptTempDirinchezmoi.toml.- Windows line endings (CRLF) in templates produce broken shell scripts on
Linux/macOS. Add
* text eol=lfto a.gitattributesin the dotfiles repo, or setcore.autocrlf = input. chezmoi initon a new machine before.chezmoi.toml.tmplexists silently skips per-machine prompts. Land that template before adding host-specific files.chezmoi doctoris the first command to run when something is off — it reports missing dependencies (op,age, etc.), broken paths, and shell integration issues in one pass.- Context7 may be unavailable. The chezmoi CLI is self-documenting via
chezmoi helpandchezmoi help <command>; fall back to that. - Two config files (e.g. both
chezmoi.tomlandchezmoi.yaml) in~/.config/chezmoi/is an error, not "first one wins". When migrating formats, delete the old file in the same commit. - Config format is detected by extension. Force it with
--config-formatif you pipe a config from an unusual path. chezmoi <unknown>looks forchezmoi-<unknown>on$PATH(git-style plugin convention) before failing. Useful for adding repo-local helpers; surprising when a typo silently runs an unrelated binary.
Reference URL index
The chezmoi docs evolve fast — new template functions, new config keys,
new prefixes every few releases. When a question goes deeper than
this skill, fetch the live page rather than guessing from training
data. Prefer mcp__tavily__tavily_extract (single URL, extract_depth: "advanced", format: "markdown") — it returns the canonical
/reference/* content directly. Use Context7 only as a fallback. Do
not tavily_map or crawl — the URLs below are stable; pick the exact
one.
Example call shape:
mcp__tavily__tavily_extract(
urls: ["https://www.chezmoi.io/reference/templates/functions/jq/"],
extract_depth: "advanced",
format: "markdown",
)
Map of what lives where:
| Topic | URL |
|---|---|
| Concepts (source / target / destination / working tree) | /reference/concepts/ |
| Source-state attribute order per target type | /reference/source-state-attributes/ |
| Target types (file / dir / symlink / script / modify / create / remove) | /reference/target-types/ |
Application order on chezmoi apply |
/reference/application-order/ |
| All command-line flags (global / common / developer) | /reference/command-line-flags/ |
Per-command reference (chezmoi <name>) |
/reference/commands/<name>/ |
| Configuration file structure | /reference/configuration-file/ |
| Hooks (read-source-state / apply / etc.) | /reference/configuration-file/hooks/ |
| Interpreters for scripts by extension | /reference/configuration-file/interpreters/ |
Special files (.chezmoiignore etc.) |
/reference/special-files/<name>/ |
Special directories (.chezmoidata/ etc.) |
/reference/special-directories/<name>/ |
Template variables (.chezmoi.os etc.) |
/reference/templates/variables/ |
Template directives (chezmoi:template:, etc.) |
/reference/templates/directives/ |
| Built-in template functions | /reference/templates/functions/<name>/ |
init template functions (promptBoolOnce etc.) |
/reference/templates/init-functions/<name>/ |
| Password-manager template functions | /reference/templates/<vendor>-functions/<name>/ (1password, bitwarden, dashlane, doppler, ejson, gopass, keepassxc, keeper, keyring, lastpass, pass, passhole, protonpass, vault, aws-secrets-manager, azure-key-vault, secret) |
| GitHub template functions | /reference/templates/github-functions/<name>/ |
| Plugins | /reference/plugins/ |
Heuristic: when the user asks "what does prefix X do" → source-state-
attributes; "what does chezmoi <cmd> do" → commands/<cmd>; "what
template var holds Y" → templates/variables; "how do I read a secret from
Z" → templates/<vendor>-functions. Fetch only the page you need with
mcp__tavily__tavily_extract — single URL, advanced depth — so the
answer is always grounded in the canonical docs as of today, not what
this skill happened to know when it was last edited.