--- title: Apply scripts description: Every run_ script chezmoi executes during apply, when each one fires, and what it does on each OS and role. --- Six scripts run during `chezmoi apply`. Four live in `.chezmoiscripts/`, a directory whose scripts run as if they sat at the source root without creating a matching target directory. Two sit at the repo root. All six are templates and all use `#!/bin/sh` with `set -eu`. ## Execution order `before_` scripts run before any file is written; `after_` scripts run after every file, symlink, and external is in place. Within a phase chezmoi runs scripts in ASCII order of their target names, so the numeric prefixes come first. Install system packages from `packages.yaml`. chezmoi writes the target state. Install gh extensions. Install and prune agent skills. Enable systemd user units on Linux (nirinit, kitty-day-save, projects-sync.timer). bat cache, font cache, cargo installs. Rebuild bat cache when its theme or config changes. Reinstall yazi plugins when the manifest changes. | Script | Phase | Frequency | Re-run trigger | | --- | --- | --- | --- | | `.chezmoiscripts/run_onchange_before_10-packages.sh.tmpl` | before | onchange | sha256 of `.chezmoidata/packages.yaml`; also the role and OS, which change the rendered package list | | `.chezmoiscripts/run_onchange_after_20-gh-extensions.sh.tmpl` | after | onchange | sha256 of `packages.shared.gh_extensions` as JSON | | `.chezmoiscripts/run_onchange_after_25-skills.sh.tmpl` | after | onchange | sha256 of `.skills` as JSON | | `.chezmoiscripts/run_once_after_30-user-services.sh.tmpl` | after | once | Runs once per distinct rendered content | | `.chezmoiscripts/run_onchange_after_40-post.sh.tmpl` | after | onchange | sha256 of `packages.shared.cargo` as JSON; also edits to the script itself or an OS change | | `run_onchange_after_build-bat-cache.sh.tmpl` | after | onchange | sha256 of `dot_config/bat/themes/cyberdream.tmTheme` and `dot_config/bat/config` | | `run_onchange_after_install-yazi-plugins.sh.tmpl` | after | onchange | sha256 of `~/.config/yazi/package.toml` (empty when the file is absent) | `run_onchange_` scripts re-run whenever their rendered content differs from the last successful run. The hash triggers work by writing a checksum of another file into a comment, so editing that file changes the script's content: ```sh # packages.yaml hash: {{ include ".chezmoidata/packages.yaml" | sha256sum }} ``` `run_once_` scripts run once per unique rendered content and again only if that content changes or the previous run failed. ## 10-packages [#10-packages] `.chezmoiscripts/run_onchange_before_10-packages.sh.tmpl` installs everything in [`packages.yaml`](/chezmoi/data#packagesyaml). It runs before files are applied so the tools exist when configs land. 1. Uses `pacman` directly when running as root, otherwise `sudo pacman`. 2. `pacman -T` filters each list down to names no installed package provides (so `mise-bin` satisfies `mise`, `yazi-git` satisfies `yazi`), then `pacman -S --needed --noconfirm` installs the rest of `packages.arch.pacman`. 3. For roles `desktop` and `laptop`, the same for `packages.arch.desktop.pacman`, and, when `pacman-conf -l` lists an `omarchy` repo, for `packages.arch.desktop.omarchy`. 4. Builds the AUR list: `packages.arch.desktop.aur` plus `packages.arch.aur` for `desktop`/`laptop`, only `packages.arch.aur` for other roles. 5. If the list is non-empty, runs `yay -S --needed --noconfirm` on it, falling back to `paru`. With neither it prints `warning: yay/paru not found — skipping AUR packages: …` and continues. 1. Exits with an error pointing at https://brew.sh if `brew` is missing. 2. `brew install` with every entry in `packages.darwin.brew`. 3. `brew install --cask` with every entry in `packages.darwin.cask`. The role is not consulted on macOS. On any other OS the script renders to just the shebang and comments and does nothing. ## 20-gh-extensions [#20-gh-extensions] `.chezmoiscripts/run_onchange_after_20-gh-extensions.sh.tmpl` installs each `packages.shared.gh_extensions` entry on every OS. - Exits 0 with `warning: gh not installed — skipping extensions` if `gh` is missing. - Exits 0 with `warning: gh not authenticated — run 'gh auth login', then 'chezmoi apply' again` if `gh auth status` fails. - Reads `gh extension list`, then runs `gh extension install ` only for extensions not already present. :::warning A skipped run still exits 0, so chezmoi records this content as done and a plain re-apply does not retry it, despite what the warning text says. After `gh auth login`, either run `gh extension install` for each entry by hand, or clear chezmoi's onchange state with `chezmoi state delete-bucket --bucket=entryState` and apply again (this re-runs every `run_onchange_` script). ::: ## 25-skills [#25-skills] `.chezmoiscripts/run_onchange_after_25-skills.sh.tmpl` installs the agent skills declared in [`skills.yaml`](/chezmoi/data#skillsyaml) with the Skills CLI (`npx -y skills@1.7.0`). See [Skills](/ai/skills). - Exits 0 with a warning if `npx` is missing. - One-time migration: deletes any directory in `~/.agents/skills` or `~/.claude/skills` that contains symlinks into the chezmoi source directory (the old vendored layout), and removes `~/.agents/.skill-lock.json` if it is a symlink into the source directory. - Removes skills listed in `${XDG_STATE_HOME:-~/.local/state}/chezmoi/skills.txt` that are no longer declared, with `skills remove -g -y`. - Runs one `skills add -g -y -a --skill ` per source entry. Output is captured; on failure the last 8 lines are printed. - Writes the declared names to `skills.txt`. Claude-only installs are not in the CLI's lock, so the script keeps its own record. - Exits 1 if any source failed, so chezmoi keeps the old hash and retries on the next apply. ## 30-user-services [#30-user-services] `.chezmoiscripts/run_once_after_30-user-services.sh.tmpl` enables session-critical systemd user units on Linux. - If `~/.config/systemd/user/nirinit.service` exists and `systemctl` is available, runs `systemctl --user daemon-reload` and `systemctl --user enable nirinit.service`. Failures are ignored. - Same check for `~/.config/systemd/user/kitty-day-save.service` (from `dot_config/systemd/user/`, so Linux desktop/laptop roles only), then `systemctl --user enable --now kitty-day-save.service`. See [kitty-day-save](/shell/scripts#kitty-day-save). - Same check for `~/.config/systemd/user/projects-sync.timer`, then `systemctl --user enable --now projects-sync.timer`. See [projects-sync](/shell/scripts#projects-sync). - On non-Linux systems it renders to `#!/bin/sh` / `exit 0`. Other personal automation units (brain-lens, omp-loop, rclone, hermes, voxtype) are deliberately not enabled; the script comment says to enable them by hand per machine. Their unit files are not in the repo, so they have to exist on the machine already. ## 40-post [#40-post] `.chezmoiscripts/run_onchange_after_40-post.sh.tmpl` is post-apply housekeeping. It re-runs when the script is edited or `packages.shared.cargo` changes. | Step | Condition | Action | | --- | --- | --- | | bat cache | `bat` installed | `bat cache --build` | | Font cache | Linux, `fc-cache` installed | `fc-cache -f` | | cargo | `cargo` installed | `cargo install --locked ` for each `packages.shared.cargo` entry missing from `cargo install --list` | Each step ignores failures (`|| true`). tpm is cloned by [`.chezmoiexternal.toml`](/chezmoi/externals-and-ignore#externals), not here. ## build-bat-cache [#build-bat-cache] `run_onchange_after_build-bat-cache.sh.tmpl` rebuilds bat's syntax and theme cache when the managed inputs change. It hashes `dot_config/bat/themes/cyberdream.tmTheme` and `dot_config/bat/config`, exits quietly if `bat` is missing, and otherwise runs `bat cache --build`. This registers the cyberdream theme and the PureScript syntax cloned by [`.chezmoiexternal.toml`](/chezmoi/externals-and-ignore#externals). ## install-yazi-plugins [#install-yazi-plugins] `run_onchange_after_install-yazi-plugins.sh.tmpl` reinstalls yazi plugins whenever `~/.config/yazi/package.toml` changes (the template hashes it via `joinPath .chezmoi.homeDir`, guarded with `stat`). It exits quietly if `ya` is missing or `${XDG_CONFIG_HOME:-$HOME/.config}/yazi/package.toml` does not exist, and otherwise runs `ya pkg install`. If that fails, it deletes ya's clone cache (`${XDG_CACHE_HOME:-$HOME/.cache}/yazi/packages`), since clones made by an older ya can break a newer one, and retries once. `~/.config/yazi` is the [prdlk/yazi](https://github.com/prdlk/yazi) external checkout, so the plugins land in its `plugins/`, which that repo's `.gitignore` excludes apart from the vendored `bunny.yazi`. The plugin list is on the [yazi page](/tools/yazi).