A hook runs commands or scripts after files matching a pattern are deployed or removed — reloading systemd after a unit file changes, re-sourcing tmux after its config changes.
hooks:
systemd-reload:
match: ["*.service", "*.timer"]
profile: arch
description: Reload systemd after unit changes
do:
- run: sudo systemctl daemon-reload
tmux:
match: .config/tmux/**/*.conf
do:
- run: tmux source-file ~/.config/tmux/tmux.conf
session-env:
match: .config/environment.d/*.conf
profile: linux
do:
- run: dbus-update-activation-environment --systemd --all
keyd:
match: /etc/keyd/*.conf
do:
- run: sudo systemctl daemon-reload
- script: keyd-restart.sh
- run: notify-send "keyd reloaded ({{ len .Files }} files)"systemd-reloadreloads systemd when a unit or timer changes.profile: archkeeps it to machines using thearchprofile.tmuxmakes the running tmux server re-read its config.session-envpasses the environment to systemd user services and D-Bus activated applications after anenvironment.dfile changes. Both are Linux only, henceprofile: linux.keydcombines step types: a command, a script, and a command rendered as a template, where.Filesholds the files that changed.
| Field | Type | Description |
|---|---|---|
match |
string or list | Target path patterns. Required |
do |
list | Steps run in order, each either run: or script:. At least one |
profile |
string | Only active under this profile, or one inheriting from it |
description |
string | Shown in the prompt, mate hooks list, and mate status |
enabled |
bool | Default true. false switches the hook off |
confirm |
bool | Default true. false runs the hook without asking — see Confirmation |
In TOML the same structure is [hooks.keyd] with [[hooks.keyd.do]] steps.
| File | Matches |
|---|---|
mate.yaml |
Files from every source |
Local config (~/.config/statemate/config.yaml) |
Files from every source |
A source's .mate.yaml |
Only that source's files, and only while the source is active |
Hooks from mate.yaml and the local config share one namespace. A local hook
with the same name replaces the repository one entirely; fields are not
merged. To switch a repository hook off on one machine:
# ~/.config/statemate/config.yaml
hooks:
systemd-reload: { enabled: false }A disabled hook needs neither match nor do.
Hooks in a source's .mate.yaml are named after the source, arch/keyd, so they
never collide with other hooks. The local config cannot override them.
Patterns match the target path, meaning where the file is deployed:
| Pattern | Matches |
|---|---|
*.service |
Any file named *.service, at any depth, under any target root |
.config/tmux/**/*.conf |
Relative to ~ |
~/.config/environment.d/*.conf |
Relative to ~, spelled out |
/etc/keyd/*.conf |
Absolute |
*matches within one path segment, and**matches any number of directories.- A pattern naming a directory matches everything under it. A trailing
/limits it to files inside the directory. - There is no
!negation, unlikeignore. A hook that should skip some files needs narrower patterns. - Attributes such as
#tmplare not part of the target path, so a pattern never needs them. Recognized source attributes in a pattern are ignored, on file and directory components alike:.config#perm-r:700/tmux/*.conf#tmplmatches the same files as.config/tmux/*.conf. Attributes do not filter matches by profile or file type. - Unknown
#suffixes stay literal. To match a literal#tmplin a target path, write[#]templateor\#tmpl. In YAML, a pattern containing a backslash must be in single quotes. target_baseandtargets:mappings make no difference: a unit deployed to/etc/systemd/systemmatches both*.serviceand/etc/systemd/system/*.service.
A hook triggers when at least one matching file was:
| Event | Command |
|---|---|
| Written, new or with changed content | mate apply |
| Removed as an orphan | mate clean |
| Removed along with its source | mate delete (not with --keep-target) |
| Renamed | mate rename — the old path and the new path are both matched |
Only what actually reached the disk counts. These do not trigger hooks:
- Permission-only fixes
#importfiles, which are copied back into the source rather than deployed- Conflicts answered with
[s]kip
A write that fails aborts the whole apply, so no hooks or #after scripts run
after it.
Scoped runs (mate apply <path>, mate apply --source <name>) trigger hooks for
the files they wrote.
In mate apply, hooks run after files and packages and before #after
scripts:
#before scripts → files → packages → hooks → #after scripts
Each triggered hook runs once, however many of its files changed. Triggered
hooks run in name order, so a numeric prefix (10-systemd, 20-keyd) forces an
order. Source hooks sort by their full <source>/<name>. Steps run in the order
listed.
A command run with sh -c. It is rendered as a template first,
with .Files set to the triggering target paths, sorted.
In a source's
.mate.yaml, the whole file is rendered once when it is loaded, before any hook runs, so{{ .Files }}there is always empty. Use the$STATEMATE_HOOK_FILESenvironment variable instead, or escape the expression so it survives the first rendering:{{`{{ .Files }}`}}.
Commands run in the directory of the config file that declared the hook: the
repository root for mate.yaml, the source directory for .mate.yaml, and
~/.config/statemate for the local config.
mate does not elevate hook commands; write sudo yourself. After mate apply
wrote system files, the sudo session it opened usually means no second password
prompt.
A script by the name mate scripts list shows in its NAME column, such as
keyd-restart.sh. The name is resolved in this order:
- For a hook in a source's
.mate.yaml, a script in that source. - A script in the repository-root
.matescripts/. - A script in any other source. If more than one matches, it is a config error.
The script runs as it would with mate scripts run: its attributes, environment,
and working directory are unchanged, and the run is recorded (except for
#always and manual scripts, which never are). If the script has a #profile:
that is not active, the step is skipped with a warning.
A script a hook ran is not run again as an #after script in the same apply.
Every step receives, on top of the usual environment:
| Variable | Value |
|---|---|
STATEMATE_HOOK_NAME |
The hook name, arch/keyd for a source hook |
STATEMATE_HOOK_FILES |
The triggering target paths, newline-separated, sorted |
STATEMATE_SOURCE_DIR |
For run: steps, the declaring source of a hook in a source's .mate.yaml. For script: steps, the script's own source |
STATEMATE_HOOK_FILES is empty when the hook is run with mate hooks run.
script: steps also receive the usual
script variables.
Hooks are confirmed like scripts, once per hook:
Run hook keyd (2 files)?
Reload keyd after config changes
- sudo systemctl daemon-reload
- script keyd-restart.sh
- notify-send "keyd reloaded (2 files)"
[Y]es / [n]o / [a]ll / [q]uit:
- Enter answers
[Y]es. [a]llconfirms the remaining hooks and the#afterscripts that follow.[q]uitstops the remaining hooks and#afterscripts. The files are already written.--forceconfirms without asking.--no-scriptsskips hooks too.- Without a terminal, hooks are skipped with a warning.
- A hook with
confirm: falseruns without asking, even without a terminal.mate hooks listshows it asnoconfirm.--no-scriptsstill skips it.
Once the files are written they are unchanged, so a hook would not trigger again
on its own. mate apply therefore records a triggered hook as pending as soon as
its files are written, and keeps it pending until it runs. A pending hook:
- is offered again on the next
mate apply, even when no file changed, with the files from both runs in.Files - is listed under "Pending hooks" by
mate status - is settled by running it with
mate hooks run <name>
A hook stays pending when the apply was stopped before it ran ([q]uit, Ctrl-C,
a package prompt quit), when it was skipped for want of a terminal or by
--no-scripts, and when it failed. Answering [n]o settles it: declining is not
remembered, so run it later with mate hooks run <name>.
Only mate apply records pending hooks. mate delete and mate clean confirm
hooks unless --force (or --noconfirm) is given, and mate rename always
asks.
A failing step stops its hook; the remaining steps are skipped. Other hooks and
the #after scripts still run, and the command exits non-zero. Under
mate apply, a failed hook stays pending and is offered again
on the next apply.
A hook is validated when it is loaded, so mate apply refuses to start and
mate check fails when:
matchis missing or a pattern does not parsedois empty, or a step has both or neither ofrunandscript- a
script:step matches no script, or more than one - a
run:template does not parse
mate doctor warns about an active hook whose patterns match no managed file,
which usually means a typo.
| Command | Purpose |
|---|---|
mate hooks list |
Every hook: NAME, SCOPE, MATCH, PROFILE, STATUS, DESCRIPTION |
mate hooks run <name> |
Run a hook now, without a prompt. --dry-run shows the steps instead |
mate apply --dry-run |
Lists the hooks that would run; -l adds the files and steps |
mate status |
Lists the hooks that pending changes would trigger |
In mate hooks list, SCOPE is repo, local, or the source name. STATUS is
disabled for a hook with enabled: false and n/a when its profile is not
active. mate hooks run refuses a disabled hook or one whose profile is not
active.
An onchange script runs when anything in its source
changes, or what its # Onchange: comment
names, and needs a file of its own. Use a hook when the follow-up needs the
changed files (.Files), runs after mate clean or mate delete, or is a single
command. Use an onchange script for longer logic, for one that should also run
when the script itself is edited, or call that script from a hook with
script:.