| name | check-author |
| description | Compose check files for the `check` infrastructure-as-code
tool. Trigger when the user wants to check or fix system
state — files, users, services, packages, network, kernel —
or learn how the design (educational, functional,
decentralized, simple) maps onto a CM idea (ansible,
sparrow, mitamae). Use this skill when the user says
"write a check", "check that X", "make sure X is true on
this host", or asks how to author a `.check` file.
|
check-author skill
You author .check files for the check infrastructure-as-code
tool. Read this whole document before writing your first @check.
1. The four design principles
These shape every decision below — each .check you author
should be defensible in their terms.
- Educational. Each check is a 5–15 line bash
construct that reads end-to-end. If you can't read the
helper or the assertion in one screen, split it.
- Functional. A check is a pure assertion; the
side-effecting
fix is named and optional. Re-running
a converged checkbook is a no-op.
- Decentralized. No agent on the node. Remote runs ship
the helper library over SSH; checks must not assume
master-side state.
- Simple. Plain bash.
check calls only account at
runtime. Don't shell out to ansible / puppet / chef.
2. The check-fix-recheck model
A check asserts state. A fix mutates state. The
runner is:
1. Run the check assertion. Exit 0 ⇒ ok, done.
2. Exit non-zero AND fix is defined ⇒ run fix.
3. Run the check assertion *again* (the SAME one).
4. Emit TAP:
ok N - <name> # was already true
not ok N - <name> # FIXED # fix worked
not ok N - <name> # FIX FAILED # fix ran, didn't work
not ok N - <name> # NO FIX # no fix defined
A good check is atomic (asserts one thing), named
(the name describes the state, not the verb), and has a
sensible fix (or no fix at all when the right answer is
"a human investigates").
Names: prefer the state over the action.
| ✗ poor name | ✓ good name |
|---|
"create user srv" | "user srv exists" |
"install nginx" | "nginx package installed" |
"open port 22" | "port 22 listening" |
3. The two DSL forms
Short form — helper as both check and default fix
@check "user srv exists" assert_user srv
The runner invokes assert_user srv for the assertion and
assert_user --fix srv for the fix. Use this for ~80% of
checks where a helper already exists.
Block form — explicit assertion and fix
@check "wireguard service enabled" {
systemctl is-enabled wg-quick@wg0
} fix {
systemctl enable wg-quick@wg0
}
Use this when no helper fits, or you need custom logic.
No-fix block — assert but don't auto-remediate
@check "no rogue listeners on 2376" {
! ss -ltn | awk '{print $4}' | grep -qE ':2376$'
} no-fix
The check fires (# NO FIX) but no fix runs. Use when the
right response is "a human investigates."
4. The helper library — when to reach for which family
See man check's HELPERS section and
share/doc/check/standards/comparison.md for the lineage.
| domain | family | helpers |
|---|
| files / directories | file | assert_file, assert_no_file, assert_dir, assert_no_dir, assert_file_contains, assert_file_not_contains, assert_file_mode, assert_file_owner, assert_symlink |
| users / groups | user | assert_user, assert_no_user, assert_group, assert_user_in_group |
| init / supervised | service | assert_service_enabled, assert_service_running, assert_no_service |
| package management | package | assert_package, assert_no_package, assert_package_version |
| network | network | assert_port_listening, assert_port_not_listening, assert_tcp_connect, assert_iptables_rule |
| kernel state | kernel | assert_kernel_module, assert_no_kernel_module, assert_sysctl |
| environment | env | assert_envvar, assert_path_contains |
Prefer helpers over hand-rolled bash. A helper has been
audited for cross-distro behaviour, idempotency, and the
fix contract; your hand-rolled if grep …; then … has not.
5. Recipes
5.1 Fresh-server baseline (see docs/check-walkthrough.md)
Ten checks, one per family. Cite the relevant tradition.
# inherits from ansible's `user` module:
# share/doc/check/standards/ansible/ansible-modules.md
@check "user srv exists" assert_user srv
@check "/srv directory exists" assert_dir /srv
@check "/srv owned by srv:srv" assert_file_owner /srv srv:srv
@check "podman package installed" assert_package podman
@check "ssh service enabled" assert_service_enabled ssh
@check "ssh service running" assert_service_running ssh
@check "port 22 listening" assert_port_listening 22 tcp
@check "ip_forward enabled" assert_sysctl net.ipv4.ip_forward 1
@check "PATH includes /srv/bin" assert_path_contains /srv/bin
@check "/etc/hosts has localhost" \
assert_file_contains /etc/hosts "127.0.0.1 localhost"
5.2 Service deployment
setup {
pkg=nginx
svc=nginx
}
@check "$pkg installed" assert_package $pkg
@check "$svc enabled" assert_service_enabled $svc
@check "$svc running" assert_service_running $svc
@check "nginx config valid" {
nginx -t 2>/dev/null
} fix {
# No safe automatic fix for a bad config — flag instead.
echo "human: fix /etc/nginx/nginx.conf" >&2
false
}
5.3 Network reachability (no fix)
@check "control plane reachable" \
assert_tcp_connect control.example.net 443
The helper has no default fix — connectivity isn't locally
fixable. The check emits # FIX FAILED (since the fix mode
exits non-zero with a hint).
5.4 Per-distro package with a version pin
@check "podman >= 4.0" assert_package_version podman ">=4.0"
assert_package_version detects the package manager and
uses the right syntax (apt: name=1.2*, dnf: name-1.2,
pacman: -S name). When pinning isn't supported by the pm,
it falls back to "install latest"; the re-check then
catches version drift.
6. Guardrails
Guardrail #1: idempotency is the contract.
A fix must be safe to run twice. Running the whole .check
file twice must produce zero # FIXED annotations the second
time (and exit 0). When the re-check fails after the fix
ran, that's a non-idempotency bug — investigate.
Guardrail #2: never write a fix that's destructive
without CHECK_ALLOW_DESTRUCTIVE.
rm -rf, userdel -r, dropping a database, applying a
schema migration — these are off-limits in the default
fix. The convention is to fail closed:
@check "no /tmp/old" {
[ ! -e /tmp/old ]
} fix {
[ "${CHECK_ALLOW_DESTRUCTIVE:-0}" = "1" ] \
|| { echo "set CHECK_ALLOW_DESTRUCTIVE=1 to rm -rf /tmp/old" >&2; exit 1; }
rm -rf /tmp/old
}
(assert_no_dir and assert_no_user already implement this
shape — use them.)
Guardrail #3: re-checks must use the same assertion.
Don't write a check that verifies the fix with a different
assertion. The loop is check → fix → SAME check. If a
different verification is needed, it's a different @check.
Guardrail #4: name the file and line on failure.
The engine's TAP output already cites <file>:<line>. Don't
swallow that with a custom error format — let the engine do
its job.
Guardrail #5: cite the tradition.
When a check maps onto a vendored reference, cite it in a
comment:
# inherits from ansible's `lineinfile` module:
# share/doc/check/standards/ansible/ansible-modules.md
@check "ip_forward in sysctl.d" \
assert_file_contains /etc/sysctl.d/99-cluster.conf \
"net.ipv4.ip_forward = 1"
The citations turn .check files into a self-teaching tour
of CM design history.
7. Common mistakes to avoid
- Doing work in the check body. The assertion should
measure, not mutate. If you need to install something
before checking, put it in the fix.
- Catching errors with
|| true in the assertion. That
turns "the state isn't right" into "the check passed,
but quietly." Let the non-zero exit propagate.
- Writing a
.check file as a sequence of echo / if
statements. That's a shell script, not a checkbook.
Wrap each idea in @check "name" { … }.
- Mixing master-side and node-side logic in one
block. A
.check file runs on the node (remote runs
copy it there). If you need master-side computation, do
it before invoking check run.
8. Where to read more
man check — full CLI surface.
docs/check.md — the contract reference.
docs/check-walkthrough.md — fresh-server in 10 checks.
share/doc/check/standards/comparison.md — guided tour of
CM history.
- The helper sources under
libexec/check/asserts/ — read
one or two to see the fail/fix pattern.