Hooks¶
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
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)"
| 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 |
In TOML the same structure is [hooks.keyd] with [[hooks.keyd.do]] steps.
Where hooks are declared¶
| File | Matches |
|---|---|
mate.yaml |
Files from every source |
Local config (~/.config/statemate/mate.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/mate.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¶
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, ** matches any number of directories, and
a pattern naming a directory matches everything under it. A trailing /
restricts a pattern to files inside the directory. Unlike
ignore, there is no ! negation: a hook that should
skip some files needs narrower patterns. Attributes such as #template are not part of the target
path, so they never need to appear in a pattern. target_base and targets:
mappings do not matter either: a unit deployed to /etc/systemd/system matches
*.service and /etc/systemd/system/*.service alike.
When hooks run¶
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.
Steps¶
run:¶
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.
script:¶
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.
Environment¶
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.
Confirmation¶
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:
[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.
Declining is not remembered. Once the files are written they are unchanged, so
the hook will not trigger on the next apply; run it with mate hooks run <name>.
mate delete and mate clean confirm hooks unless --force is given, and
mate rename always asks.
Failure¶
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. A failed hook is
not retried on the next apply.
Checking¶
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.
Commands¶
| Command | Purpose |
|---|---|
mate hooks list |
Every hook: NAME, SCOPE, MATCH, PROFILE, STEPS, 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; -V 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.
Hooks or #onchange scripts¶
An onchange script runs when anything in its source
changes and needs a file of its own. Use a hook when the follow-up depends on
which files changed, or when it is a single command. Use an onchange script
for longer logic tied to a whole source, or call that script from a hook with
script:.