command
Runs an arbitrary shell command on the target host. The resource name is a user-chosen identifier (e.g. install-node) used in reports; it does not refer to a system object.
Idempotency is your responsibility
Unlike package, file, and service, the command resource has no natural notion of "desired state". Without a guard attribute, every apply will execute the command and report CHANGED. Use one of the guards below — creates, unless, or onlyif — to make the resource idempotent.
Security
The command runs as the agent's user. In typical deployments that means root. Treat manifests as privileged code; anyone who can write a manifest effectively has a shell on every target host.
Attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
command | string | yes | The shell command to run. Executed via /bin/sh -c "<command>". |
creates | string | no | Skip the command if this path already exists. |
unless | string | no | Skip the command if this shell command exits 0. |
onlyif | string | no | Skip the command if this shell command exits non-zero. |
Guards are combined with AND: the command runs only if every provided guard indicates "run".
Behavior
Check evaluates the guards in order:
- If
createsis set and the path exists, the resource is already satisfied (reported asOK). - If
unlessis set, Maddock runs it through/bin/sh -cand treats exit0as "already done". - If
onlyifis set, Maddock runs it and treats exit non-zero as "precondition not met — skip". - Otherwise the resource reports
CHANGEDwith a singlecommanddifference.
Apply runs the command through /bin/sh -c "<command>". Exit 0 → CHANGED. Non-zero exit → FAILED, with stderr included in the error message.
With --dry-run, only the check phase runs. Resources that would execute are reported as SKIPPED and the process exits with code 3.
Examples
Idempotent via creates
The most common pattern: skip the command if a file it produces already exists.
- command:
install-node:
command: "curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -"
creates: /etc/apt/sources.list.d/nodesource.listIdempotent via unless
Useful when the "done" signal is a command's exit code rather than a file on disk.
- command:
add-sudo-group:
command: "usermod -aG sudo alice"
unless: "id -nG alice | grep -qw sudo"Idempotent via onlyif
The inverse of unless: run only when a precondition is met.
- command:
reboot-if-needed:
command: "systemctl reboot"
onlyif: "test -f /var/run/reboot-required"Combining guards
Guards are AND-combined. Both must say "run" for the command to execute.
- command:
provision-db:
command: "psql -U postgres -c 'CREATE DATABASE app'"
unless: "psql -U postgres -lqt | cut -d '|' -f 1 | grep -qw app"
onlyif: "systemctl is-active --quiet postgresql"Gotchas
- Shell semantics apply. Pipes, redirects, and environment variable expansion all work because the command is invoked via
/bin/sh -c. On most Linux distributions/bin/shis bash-compatible, but scripts that rely on bash-only features should invoke bash explicitly:command: "bash -c '...'". - No
daemon-reloador restart hook. Commands do not automatically reload systemd or restart services. Model that explicitly with a subsequentserviceresource or anothercommand. createsis not a trigger. It only controls whether the command runs this time; it does not delete the path or refresh it.
Known limitations
- No
cwd/ working directory (commands run in the agent's CWD). - No
user/ sudo-style user switching (commands run as the agent's user). - No
environment/ custom env vars (inherits the agent's environment).
These are planned additions; see the project roadmap.