shell
The shell module executes commands on the remote host via SSH.
shell "echo 'hello world'"
shell "curl -sf http://localhost:8080/health" { retries 5 delay 3}Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
| (positional) | string | The command to execute (alternative to cmd) |
cmd | string or list | Single command string, or list of commands joined with && (alternative to positional) |
check | string | Gate command — if it exits 0 the step is skipped (already satisfied) |
retries | integer | Number of retry attempts on failure |
delay | integer | Seconds between retries |
login | boolean | Run the command (and check gate) inside a POSIX login shell so /etc/profile and ~/.profile are sourced |
Conditional execution with check
Section titled “Conditional execution with check”By default the shell module always runs. The optional check parameter runs a gate command first to decide whether the step is needed:
- Exit 0 — the step is already satisfied and is skipped
- Non-zero exit — the step is pending and will run
step "Install package" { shell "apt-get install -y nginx" check="dpkg -l nginx | grep -q ^ii"}Using cmd instead of positional
Section titled “Using cmd instead of positional”The cmd parameter can be used as an alternative to the positional command string. It accepts either a single string or a list of commands (joined with &&).
Single command
Section titled “Single command”When combined with check, this provides a clean block syntax with no positional argument needed:
step "Start valkey" { shell { check "docker ps --filter name=prophet-valkey --filter status=running -q | grep -q ." cmd "docker run -d --name prophet-valkey --network prophet --restart always -p 6379:6379 -v prophet_valkey:/data valkey/valkey:8-alpine" }}Command list (multiline)
Section titled “Command list (multiline)”For long command sequences, use a list. The commands are joined with &&:
step "Add deadsnakes PPA" { shell { check "test -f /etc/apt/sources.list.d/deadsnakes-*" cmd { - "apt-get update -qq" - "apt-get install -y software-properties-common" - "add-apt-repository -y ppa:deadsnakes/ppa" - "apt-get update -qq" } }}This is equivalent to:
shell "apt-get update -qq && apt-get install -y software-properties-common && add-apt-repository -y ppa:deadsnakes/ppa && apt-get update -qq"Login shell environment (login=#true)
Section titled “Login shell environment (login=#true)”SSH non-interactive sessions start with a minimal environment. Profile scripts in /etc/profile, /etc/profile.d/*.sh, and ~/.profile — which is where Nix, asdf, nvm, rustup, and similar tools inject their PATH entries — are not sourced by default. That means a command like shell "rg foo" will often fail with command not found even though the tool is installed.
Set login=#true to wrap the command (and the check gate) in sh -l -c '…', which forces the remote to read those profile scripts:
// Nix-installed toolshell "rg TODO ./src" login=#true
// With a check gateshell { cmd "mytool --refresh" check "command -v mytool" login #true}Use this whenever the tool lives in a user profile or uses shims (~/.nix-profile/bin, ~/.asdf/shims, ~/.nvm/versions/...). You do not need it for tools in system paths like /usr/bin or /usr/local/bin.
See also the nix module for higher-level package/shell/build operations that set up their own Nix environment.
Idempotency
Section titled “Idempotency”Without a check parameter, the shell module always reports Pending — it has no way to know if the command needs to run. Use check to make shell steps idempotent, or use the module for commands that are safe to repeat.
Examples
Section titled “Examples”Simple command
Section titled “Simple command”step "Check connectivity" { shell "ping -c 1 google.com"}Health check with retries
Section titled “Health check with retries”step "Wait for app" { shell "curl -sf http://localhost:8080/health" { retries 10 delay 5 }}Capture output with register
Section titled “Capture output with register”step "Get hostname" { shell "hostname" register="node_hostname"}
step "Log it" { shell "echo 'Running on ${node_hostname}'"}Skip if already done
Section titled “Skip if already done”step "Initialize database" { shell "pg_isready && createdb myapp" check="psql -lqt | grep -q myapp"}See Loops & Register for more on capturing command output.