Understand how scripts work
chezmoi supports scripts that execute when you run chezmoi apply. Scripts are files in the source directory with the run_ prefix, executed in alphabetical order.
Script types
run_ Execute every time you run chezmoi apply
run_onchange_ Execute only when script contents have changed
run_once_ Execute once per unique content version
All scripts should be idempotent (safe to run multiple times). Scripts break chezmoi’s declarative approach and should be used sparingly.
Script timing
Scripts normally run while chezmoi updates your dotfiles:
Use before_ or after_ attributes to control timing:
Before updates
After updates
run_once_before_install-password-manager.sh
Script requirements
Create manually
chezmoi cd
touch run_install-packages.sh
chmod +x run_install-packages.sh
No need to set executable bit - chezmoi handles it.
Include shebang or use binary
#!/bin/bash
echo "Installing packages..."
Scripts must include a #! line or be an executable binary.
Template support
{{ if eq .chezmoi.os "linux" -}}
#!/bin/sh
sudo apt install ripgrep
{{ else if eq .chezmoi.os "darwin" -}}
#!/bin/sh
brew install ripgrep
{{ end -}}
Scripts with .tmpl suffix are templated. If template resolves to whitespace, script won’t execute.
Scripts in .chezmoiscripts/ directory are executed without creating a corresponding directory in the target state.
Set environment variables
Configure extra environment variables for scripts in your config:
~/.config/chezmoi/chezmoi.toml
[ scriptEnv ]
MY_VAR = "my_value"
API_KEY = "{{ onepassword " api-key" }}"
chezmoi automatically sets:
CHEZMOI=1
CHEZMOI_OS (e.g., linux, darwin)
CHEZMOI_ARCH (e.g., amd64, arm64)
Other template data as CHEZMOI_* variables
Don’t show scripts in chezmoi diff/chezmoi status
Exclude from diff
Exclude from status
~/.config/chezmoi/chezmoi.toml
[ diff ]
exclude = [ "scripts" ]
Scripts won’t appear in chezmoi diff output. ~/.config/chezmoi/chezmoi.toml
[ status ]
exclude = [ "scripts" ]
Scripts won’t show R status in chezmoi status.
Install packages with scripts
Create installation script
chezmoi cd
touch run_onchange_install-packages.sh
Add package installation commands
Simple version
Template version
#!/bin/sh
sudo apt install ripgrep
Apply changes
Script runs on first apply. With run_onchange_, it won’t run again unless contents change.
Name your template run_onchange_install-packages.sh.tmpl to install platform-specific packages automatically.
Run a script when another file changes
Use content checksums to trigger scripts when other files change:
run_onchange_dconf-load.sh.tmpl
dconf.ini
#!/bin/bash
# dconf.ini hash: {{ include "dconf.ini" | sha256sum }}
dconf load / < {{ joinPath .chezmoi.sourceDir "dconf.ini" | quote }}
How it works:
Template includes SHA256 hash of dconf.ini in a comment
When dconf.ini changes, the hash changes
Changed hash means changed script content
chezmoi reruns the run_onchange_ script
Add dconf.ini to .chezmoiignore so chezmoi doesn’t create it in your home directory.
Clear the state of all run_onchange_ and run_once_ scripts
chezmoi tracks script execution in persistent state.
chezmoi state delete-bucket --bucket=entryState
All run_onchange_ scripts will run again on next chezmoi apply. chezmoi state delete-bucket --bucket=scriptState
All run_once_ scripts will run again on next chezmoi apply.
Clearing state causes scripts to run again. Ensure your scripts are idempotent before doing this.
Real-world examples
run_onchange_before_install-homebrew.sh.tmpl
run_once_after_install-oh-my-zsh.sh
run_onchange_update-packages.sh.tmpl
{{ if eq .chezmoi.os "darwin" -}}
#!/bin/bash
if ! command -v brew & > /dev/null; then
echo "Installing Homebrew..."
/bin/bash -c "$( curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
{{- end }}