Run Scripts Deep Dive

Lesson 8 · chezmoi · ~10 minutes

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.

The Four Script Types

run_once

Runs exactly once per machine. Chezmoi tracks which scripts have already executed and skips them on future applies.

run_onchange

Runs only when the script's content changes. Chezmoi hashes the file — if the hash differs from last run, it re-executes.

run_before

Runs before chezmoi updates any files. Use for setup tasks that managed files depend on.

run_after

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.

Execution Order

Chezmoi runs scripts in a strict order during chezmoi apply:

Script execution order during chezmoi apply
run_before scripts
File updates
run_after scripts

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).

How run_onchange Detects Changes

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.

Key insight

The run_onchange hash is computed after template execution. So template variables, includes, and conditionals all contribute to whether the script re-runs.

The .chezmoiscripts/ Directory

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.

Idempotency Best Practices

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.

Guard your scripts

Always check whether work has already been done before doing it. Don't assume a clean slate.

Common patterns for idempotent scripts:

PatternExample
Check before installcommand -v brew >/dev/null || /bin/bash -c "$(curl ...)"
Check before directory createmkdir -p ~/.config/myapp (already idempotent)
Use package manager flagsbrew install --quiet pkg (no-op if installed)
Check file existence[ -f ~/.ssh/id_ed25519 ] || ssh-keygen ...

Exercise: Setup and Reload Scripts

Step 1: Create the scripts directory

chezmoi cd
mkdir -p .chezmoiscripts

Step 2: Create a run_once script for Xcode CLI tools

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

Step 3: Create a run_after script to reload shell config

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
Why not source directly?

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.

Step 4: Test with a dry run

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.

Step 5: Apply and commit

chezmoi apply
git add .chezmoiscripts/
git commit -m "chore: add xcode-cli install and shell reload scripts"
git push
exit

What just happened

ScriptTypeBehavior
run_once_before_install-xcode-cli.shrun_once + beforeRuns once per machine, before file updates. Checks if Xcode CLI is present before installing.
run_after_reload-shell.shrun_afterRuns on every apply, after file updates. Reminds you to reload your shell.

Knowledge Check

What determines the execution order of scripts within the same phase (before or after)?
Correct. Chezmoi sorts scripts lexicographically by filename within each phase. Use numeric prefixes like 01-, 02- to control order explicitly.
Chezmoi uses lexicographic (alphabetical) order by filename. This is why numeric prefixes like 01-, 02- are the standard pattern for controlling execution sequence.
How does run_onchange decide whether to re-run a script?
Right. Chezmoi hashes the script's content after template execution. If the hash matches what's stored, the script is skipped. This means template variable changes can trigger re-execution.
Chezmoi doesn't use timestamps. It computes a SHA-256 hash of the script after template execution and compares it to the stored hash from the last run. A hash mismatch triggers re-execution.
A script named run_once_before_setup.sh — when does it execute?
Exactly. run_once means it executes only once (chezmoi remembers). before means it runs in the pre-file-update phase. The attributes combine.
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.

Next up

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.

Recommended Reading

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.

Questions? Ask me anything that's unclear. I can explain the content hashing mechanism in more detail, help you debug script execution order, or design scripts for your specific setup needs.
← Prev Next →