Ignition
ReferenceResources

exec

Run arbitrary commands on remote hosts.

The exec resource runs commands on the remote host via SSH. It is imperative by default, with apply-time preconditions (unless / onlyIf) for conditional execution and explicitly unsafe check-time escape hatches when you need them.

Input

FieldTypeDefaultRequiredDescription
commandstringYesThe command to run
sudobooleanfalseNoRun with sudo sh -c
cwdstringNoWorking directory
shellstringNoPOSIX shell preamble run before command in the same shell session
envRecord<string, string>NoEnvironment variables
checkbooleantrueNoIf false, non-zero exit codes are not failures
unlessstringNoSkip apply when this precondition exits 0; evaluated in apply()
onlyIfstringNoRun apply only when this precondition exits 0; evaluated in apply()
unsafeCheckUnlessstringNoUnsafe: skip apply when this command exits 0 during check()
unsafeCheckOnlyIfstringNoUnsafe: run apply only when this command exits 0 during check()

Output

FieldTypeDescription
exitCodenumberExit code of the command
stdoutstringStandard output
stderrstringStandard error

Behavior

Check phase

check() is read-only by default. It does not execute command, unless, or onlyIf.

By default:

  • unguarded exec returns inDesiredState: false
  • unless / onlyIf also return inDesiredState: false
  • ignition run --check therefore reports "would change" conservatively for those cases

Unsafe opt-ins:

  • unsafeCheckUnless: if the command exits 0, the resource is treated as already satisfied
  • unsafeCheckOnlyIf: if the command exits non-zero, apply is skipped
  • unsafeCheckUnless and unsafeCheckOnlyIf execute during check(), including ignition run --check
  • only one of unless, onlyIf, unsafeCheckUnless, and unsafeCheckOnlyIf may be set

Apply phase

  1. Evaluates unless / onlyIf first, if present.
  2. Builds the command string:
    • Wraps with cd <cwd> && if cwd is specified
    • Prepends environment variables as KEY=VALUE pairs
    • If shell is set, runs shell and command inside the same sh -c session
    • Wraps with sudo sh -c '...' if sudo is true
  3. Executes the command via SSH.
  4. If the command exits with non-zero and check is true, throws SSHCommandError.
  5. If check is false, the exit code is captured but not treated as a failure.

Guard commands inherit sudo, cwd, env, and shell from the parent input. A failing shell preamble aborts guard evaluation instead of being treated like a false unless / onlyIf.

Annotations

PropertyValue
NatureImperative
IdempotentNo
DestructiveYes
Read-onlyNo
Required capabilitiesexec

Examples

Basic command

await exec({ command: "nginx -t" })

With sudo

await exec({ command: "systemctl daemon-reload", sudo: true })

With working directory

await exec({ command: "npm install --production", cwd: "/opt/app" })

With environment variables

await exec({
  command: "npm run build",
  cwd: "/opt/app",
  env: { NODE_ENV: "production", CI: "true" },
})

With a shell preamble

await exec({
  command: "nvm install 24 && nvm alias default 24",
  shell: 'export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh"',
  unless: "node --version | grep -q '^v24'",
})

Tolerating non-zero exit codes

await exec({ command: "grep -q 'done' /tmp/flag", check: false })

With check: false, the resource won't fail even if the command returns a non-zero exit code. The exit code is still captured in the output.

Guarded execution

await exec({
  command: "npm install -g pm2",
  unless: "command -v pm2",
  sudo: true,
})

Unsafe check-time probe

await exec({
  command: "npm install -g pm2",
  unsafeCheckUnless: "command -v pm2",
  sudo: true,
})

Capturing output

const result = await exec({ command: "cat /etc/hostname" })
const hostname = result.output?.stdout.trim()

Gotchas

  • Unguarded exec always reports changed because it can't determine whether the command mutated state.
  • In check mode (ignition run --check), unguarded exec reports as "would change" since it's imperative.
  • unless and onlyIf are apply-time preconditions, so check mode still reports them as "would change".
  • unless can report ok during apply when the precondition shows the desired state is already met.
  • onlyIf can also report ok, but that means the precondition failed and apply was skipped.
  • unsafeCheckUnless and unsafeCheckOnlyIf are the only ways to run user commands during check(); use them only when you intentionally accept dry-run side effects.
  • Set check: false for commands where a non-zero exit code is acceptable (e.g., grep returning 1 for no match).
  • sudo wraps the entire command with sudo sh -c '...' — shell features like pipes and redirects work inside.
  • shell runs through POSIX sh -c, not Bash. Use portable shell syntax, or invoke bash -lc '...' yourself when you need Bash-specific behavior.
  • command, shell, and guard fields are trusted shell snippets. Do not pass untrusted input into them without your own validation/escaping.
  • Command output (stdout/stderr) can be streamed in real time when trace mode is enabled.

On this page