Skip to content

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
}
ParameterTypeDescription
(positional)stringThe command to execute (alternative to cmd)
cmdstring or listSingle command string, or list of commands joined with && (alternative to positional)
checkstringGate command — if it exits 0 the step is skipped (already satisfied)
retriesintegerNumber of retry attempts on failure
delayintegerSeconds between retries
loginbooleanRun the command (and check gate) inside a POSIX login shell so /etc/profile and ~/.profile are sourced

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"
}

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 &&).

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"
}
}

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"

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 tool
shell "rg TODO ./src" login=#true
// With a check gate
shell {
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.

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.

step "Check connectivity" {
shell "ping -c 1 google.com"
}
step "Wait for app" {
shell "curl -sf http://localhost:8080/health" {
retries 10
delay 5
}
}
step "Get hostname" {
shell "hostname" register="node_hostname"
}
step "Log it" {
shell "echo 'Running on ${node_hostname}'"
}
step "Initialize database" {
shell "pg_isready && createdb myapp" check="psql -lqt | grep -q myapp"
}

See Loops & Register for more on capturing command output.