Chezmoi doesn't just manage files — it can run scripts at specific points during chezmoi apply. This is how you automate package installs, reload shell configs, or set up system preferences. By the end of this lesson, you'll understand all four script types and when to use each one.
Runs exactly once per machine. Chezmoi tracks which scripts have already executed and skips them on future applies.
Runs only when the script's content changes. Chezmoi hashes the file — if the hash differs from last run, it re-executes.
Runs before chezmoi updates any files. Use for setup tasks that managed files depend on.
Runs after chezmoi finishes updating files. Use for reload commands or post-setup tasks.
These attributes combine. A script named run_once_before_install-packages.sh runs once, before file updates. A script named run_onchange_after_reload.sh runs after file updates, but only when its contents change.
Chezmoi runs scripts in a strict order during chezmoi apply:
Within each phase (before or after), scripts execute in lexicographic order by filename. This means you control execution order with numeric prefixes:
run_once_before_01-install-homebrew.sh
run_once_before_02-install-packages.sh
run_after_reload-shell.sh
Homebrew installs first (01), then packages (02), because 01 sorts before 02. Scripts without before or after in their name run after file updates (same as run_after).
Chezmoi computes a SHA-256 hash of the script's contents after template execution. It stores this hash in its persistent state. On the next chezmoi apply, it re-hashes the script — if the hash matches, the script is skipped.
This is powerful for template-based scripts. If you use a template variable in the script body, the hash changes when the variable changes:
# .chezmoiscripts/run_onchange_after_configure-git.sh.tmpl
#!/bin/bash
# hash: {{ include "dot_gitconfig.tmpl" | sha256sum }}
git config --global core.autocrlf input
The include | sha256sum trick forces the script to re-run whenever .gitconfig changes — even though the actual command hasn't changed. The hash in the comment changes the script's content hash.
The run_onchange hash is computed after template execution. So template variables, includes, and conditionals all contribute to whether the script re-runs.
You can place scripts directly in your source root, but as your collection grows this gets messy. Chezmoi supports a dedicated .chezmoiscripts/ directory to keep scripts organized separately from managed files:
~/.local/share/chezmoi/
├── .chezmoiscripts/
│ ├── run_once_before_01-install-xcode-cli.sh
│ ├── run_once_before_02-install-homebrew.sh
│ ├── run_once_before_03-install-packages.sh
│ └── run_after_reload-shell.sh
├── dot_gitconfig
├── dot_zshrc.tmpl
└── .chezmoi.toml.tmpl
Scripts in .chezmoiscripts/ behave identically to scripts in the source root. The directory just provides organization — chezmoi discovers and runs them the same way.
A script is idempotent if running it multiple times produces the same result as running it once. This matters because run_before and run_after scripts run on every apply. Even run_once scripts should be idempotent in case you reset chezmoi's state.
Always check whether work has already been done before doing it. Don't assume a clean slate.
Common patterns for idempotent scripts:
| Pattern | Example |
|---|---|
| Check before install | command -v brew >/dev/null || /bin/bash -c "$(curl ...)" |
| Check before directory create | mkdir -p ~/.config/myapp (already idempotent) |
| Use package manager flags | brew install --quiet pkg (no-op if installed) |
| Check file existence | [ -f ~/.ssh/id_ed25519 ] || ssh-keygen ... |
chezmoi cd
mkdir -p .chezmoiscripts
Create .chezmoiscripts/run_once_before_install-xcode-cli.sh:
#!/bin/bash
# Install Xcode Command Line Tools if not already present.
if ! xcode-select -p &>/dev/null; then
echo "Installing Xcode Command Line Tools..."
xcode-select --install
# Wait for installation to complete.
until xcode-select -p &>/dev/null; do
sleep 5
done
echo "Xcode CLI tools installed."
else
echo "Xcode CLI tools already installed."
fi
Make it executable:
chmod +x .chezmoiscripts/run_once_before_install-xcode-cli.sh
Create .chezmoiscripts/run_after_reload-shell.sh:
#!/bin/bash
# Reload shell configuration after dotfile updates.
if [ -n "$ZSH_VERSION" ] || [ "$SHELL" = "/bin/zsh" ]; then
echo "Shell config updated. Run 'source ~/.zshrc' to reload in this session."
elif [ -n "$BASH_VERSION" ] || [ "$SHELL" = "/bin/bash" ]; then
echo "Shell config updated. Run 'source ~/.bashrc' to reload in this session."
fi
Make it executable:
chmod +x .chezmoiscripts/run_after_reload-shell.sh
Chezmoi runs scripts in a subshell. Running source ~/.zshrc inside the script would reload the config in that subshell only — not in your active terminal. Instead, print a reminder for the user.
chezmoi apply --dry-run --verbose
You should see both scripts listed in the output. The run_once_before script will show up in the "before" phase, and the run_after script in the "after" phase.
chezmoi apply
git add .chezmoiscripts/
git commit -m "chore: add xcode-cli install and shell reload scripts"
git push
exit
| Script | Type | Behavior |
|---|---|---|
run_once_before_install-xcode-cli.sh | run_once + before | Runs once per machine, before file updates. Checks if Xcode CLI is present before installing. |
run_after_reload-shell.sh | run_after | Runs on every apply, after file updates. Reminds you to reload your shell. |
01-, 02- to control order explicitly.01-, 02- are the standard pattern for controlling execution sequence.run_onchange decide whether to re-run a script?run_once_before_setup.sh — when does it execute?run_once means it executes only once (chezmoi remembers). before means it runs in the pre-file-update phase. The attributes combine.run_once means it only executes once per machine (tracked in chezmoi's state), and before means it runs before file updates. Together: once per machine, in the before phase.Scripts handle setup tasks, but what about pulling in files you don't own — like fonts, binaries, or archives from the internet? In Lesson 9: External Files, you'll learn to declare external dependencies in .chezmoiexternal.toml and have chezmoi fetch them automatically.
chezmoi: Use Scripts to Perform Actions — The full reference on script types, ordering, and template integration. Covers edge cases like run_onchange with .tmpl extensions. ~8 minute read.