Chezmoi doesn't just manage file contents — it can run scripts during chezmoi apply. This lesson teaches you to automate package installation with Homebrew Bundles, conditionally tailored per machine using templates and chezmoi data.
Scripts are actions chezmoi executes alongside file management. They live in your source state with special filename prefixes that control when they run:
Chezmoi recognizes two primary script prefixes that control execution behavior:
| Prefix | Behavior | Use case |
|---|---|---|
run_once_ | Runs exactly once, ever. Chezmoi records that it ran and never runs it again. | One-time setup (create directories, initial config) |
run_onchange_ | Runs when the script's contents change. Chezmoi hashes the rendered output and re-runs only when the hash differs. | Package installation, config compilation |
There are also run_before_ and run_after_ variants that run on every chezmoi apply, but those are less common. For package management, run_onchange_ is what you want — it re-runs only when your package list actually changes.
run_onchange_ scripts are the sweet spot for package management. They're idempotent by nature: add a package to your list → the hash changes → chezmoi re-runs the script → brew bundle installs only what's missing.
The full filename pattern for scripts is:
run_onchange_[before_|after_]description.sh[.tmpl]
Let's break that down:
| Part | Purpose |
|---|---|
run_onchange_ | Execution trigger (once or onchange) |
before_ / after_ | Optional. Controls whether it runs before or after file updates. |
description | Human-readable name. Determines sort order (scripts run alphabetically). |
.sh | Script extension (determines interpreter) |
.tmpl | Optional. Makes it a template that gets rendered before execution. |
Scripts execute in alphabetical order by filename. Use naming like 01-install-packages, 02-configure-defaults to control sequence explicitly.
Chezmoi stores script execution records in its persistent state database (~/.config/chezmoi/chezmoistate.boltdb):
chezmoi state delete-bucket --bucket=scriptState.apply, it re-renders the script, computes the hash, and compares. If the hash differs (because you added a package, changed a template variable, etc.), it re-runs.Since run_onchange_ hashes the rendered output, changing your .chezmoidata (like switching machine_type) will change the rendered Brewfile, which changes the hash, which triggers a re-run. This is exactly what you want — a new machine type means different packages to install.
You'll create a run_onchange_ script that generates a Brewfile from a template and runs brew bundle. Packages will vary based on machine_type from your .chezmoidata.yaml.
Your ~/.local/share/chezmoi/.chezmoidata.yaml should already have this from Lesson 3:
machine_type: work # or "personal" or "server"
Create the script in your source state:
chezmoi cd
Create a file called run_onchange_install-packages.sh.tmpl in the source root:
#!/bin/bash
# Chezmoi-managed Brewfile — do not edit directly.
# This script re-runs whenever the rendered package list changes.
set -euo pipefail
# Generate a temporary Brewfile from the template
BREWFILE=$(mktemp)
trap 'rm -f "$BREWFILE"' EXIT
cat > "$BREWFILE" << 'BREWFILE_CONTENTS'
# Core packages — all machines
brew "git"
brew "ripgrep"
brew "fd"
brew "jq"
brew "chezmoi"
{{ if eq .machine_type "work" -}}
# Work-specific packages
brew "awscli"
brew "terraform"
brew "kubectl"
brew "helm"
cask "slack"
cask "zoom"
{{- end }}
{{ if eq .machine_type "personal" -}}
# Personal-specific packages
brew "yt-dlp"
brew "ffmpeg"
cask "vlc"
cask "obsidian"
{{- end }}
{{ if or (eq .machine_type "work") (eq .machine_type "personal") -}}
# GUI machines get development tools
brew "gh"
brew "lazygit"
cask "wezterm"
cask "visual-studio-code"
{{- end }}
BREWFILE_CONTENTS
# Install packages using the generated Brewfile
brew bundle --file="$BREWFILE" --no-lock
The .tmpl suffix tells chezmoi to render this through Go's template engine before execution. The rendered output (with conditionals resolved) is what chezmoi hashes to detect changes.
Before applying, see the rendered script:
chezmoi cat run_onchange_install-packages.sh.tmpl
You'll see the template conditionals resolved for your current machine_type. Only the relevant package blocks appear.
chezmoi apply --verbose
You'll see chezmoi execute the script. It renders the template, detects a new hash (first run), and runs it. Brew bundle will install any missing packages from the generated Brewfile.
Run apply again:
chezmoi apply --verbose
This time the script should not run — the rendered hash hasn't changed. Now add a package to your template, apply again, and watch it trigger.
chezmoi cd
git add run_onchange_install-packages.sh.tmpl
git commit -m "feat: add templated brew bundle script"
git push
exit
| Concept | What it does |
|---|---|
run_onchange_ prefix | Script runs only when its rendered content changes |
.tmpl suffix | Renders through Go template engine before execution/hashing |
| Template conditionals | Include/exclude packages based on .chezmoidata values |
brew bundle --no-lock | Installs packages without creating a lockfile |
| Hash tracking | Chezmoi stores rendered script hash to detect future changes |
If you have multiple scripts (package install, macOS defaults, directory creation), prefix with numbers: run_onchange_01-install-packages.sh.tmpl, run_once_02-configure-macos.sh.tmpl. They execute in lexicographic order.
run_onchange_ script re-runs on chezmoi apply?machine_type from "personal" to "work" in .chezmoidata.yaml. What happens on the next chezmoi apply?machine_type changes the rendered output, which changes the hash. Chezmoi detects the new hash and re-runs the script — installing the correct packages for the new machine type.01-, 02- to control execution sequence explicitly.01-, 02- are useful for controlling sequence.Your package installs are now automated and machine-aware. But many configs need secrets — API tokens, SSH keys, credentials. In Lesson 5: Secrets & Keychain, you'll learn how chezmoi integrates with macOS Keychain and other secret managers to template sensitive values without storing them in your repo.
chezmoi: Use Scripts to Perform Actions — The official script reference. Covers all script prefixes, interpreters, and the state tracking system in detail. ~8 minute read.