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

1. **10-packages (before, onchange)**

    Install system packages from `packages.yaml`.

2. **Files, symlinks, externals**

    chezmoi writes the target state.

3. **20-gh-extensions (after, onchange)**

    Install gh extensions.

4. **30-user-services (after, once)**

    Enable systemd user units on Linux.

5. **40-post (after, onchange)**

    tpm clone, bat cache, font cache.

6. **build-bat-cache (after, onchange)**

    Rebuild bat cache when its theme or config changes.

7. **install-yazi-plugins (after, onchange)**

    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_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 | Only 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 `dot_config/yazi/package.toml` |

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

**Linux**

1. Uses `pacman` directly when running as root, otherwise `sudo pacman`.
2. `pacman -S --needed --noconfirm` with every entry in `packages.arch.pacman`.
3. For roles `desktop` and `laptop`, a second `pacman -S --needed --noconfirm` with `packages.arch.desktop.pacman`.
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 and `paru` exists, runs `paru -S --needed --noconfirm` on it. Without `paru` it prints `warning: paru not found — skipping AUR packages: …` and continues.

**macOS**

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 <repo>` 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).
:::

## 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.
- 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. The repo does not currently contain a `dot_config/systemd/` directory, so the unit file has to exist on the machine already for this script to do anything.

## 40-post [#40-post]

`.chezmoiscripts/run_onchange_after_40-post.sh.tmpl` is post-apply housekeeping. It has no hash comment, so it only re-runs when the script itself is edited.

| Step | Condition | Action |
| --- | --- | --- |
| tmux plugin manager | `tmux` installed and `~/.config/tmux/plugins/tpm` missing | `git clone --depth=1 https://github.com/tmux-plugins/tpm ~/.config/tmux/plugins/tpm` |
| bat cache | `bat` installed | `bat cache --build` |
| Font cache | Linux, `fc-cache` installed | `fc-cache -f` |

Each step ignores failures (`|| true`). `tmux.conf` expects tpm at that path; see [tmux](/terminal/tmux).

## 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 `dot_config/yazi/package.toml` changes. 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`. The installed plugins land in `~/.config/yazi/plugins/`, which `.chezmoiignore.tmpl` excludes. The plugin list is on the [yazi page](/tools/yazi).
