# Commands and scripts

A step can run raw shell instead of a tool. It is always available, and it costs you the preview, the change report and idempotence.

Source: https://opafra.com/docs/plans/commands-and-scripts

---

Not everything you need is a tool. A step can run an arbitrary shell command with
`ssh.cmd`, or upload and execute a script with `ssh.script.file`. Both are deliberately
available and deliberately weaker than a named tool.

## The trade-off, stated plainly

A named tool declares what it does, so Opafra can reason about it. A raw command is
opaque text, so it cannot.

| | Named tool | Raw command |
|---|---|---|
| Dry run | Can predict the outcome on 15 of them | Shows you the resolved text, predicts nothing |
| Changed vs unchanged | Reported | Unknown; an exit code is not a change report |
| Idempotent | Where the tool says so | Only if you wrote it that way |
| Failure | Reported per host | Reported per host |

This is not a limitation Opafra could engineer away. `apt-get install -y nginx` and
`rm -rf /var/lib/mysql` are both a string, and nothing can tell you what an arbitrary
string will do to a host without running it.

## Prefer a tool where one fits

The clearest example is a line in a config file:

```yaml title="do this"
- name: Disable password auth
  tool: file.lineinfile
  args:
    path: /etc/ssh/sshd_config
    regexp: "^#?PasswordAuthentication"
    line: "PasswordAuthentication no"
```

```yaml title="not this"
- name: Disable password auth
  tool: ssh.cmd
  args:
    command: echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
```

Both work the first time. The second appends a duplicate line every subsequent run, and
after four runs the file has four copies. The first is idempotent, its dry run reads the
current file and shows you the diff, and rerunning it reports no change.

The [tool catalogue](/docs/reference/tools) lists everything available. Reach for a
command when nothing there fits, not by default.

## Running a command

```yaml
- name: Check the queue depth
  targets: [worker-01]
  tool: ssh.cmd
  args:
    command: "systemctl show -p NRestarts {{ vars.service }}"
```

Variables are substituted before the command is sent. Capture its output if a later step
needs it, rather than running it twice:

```yaml
  capture:
    var: restarts
    mode: regex
    pattern: "NRestarts=([0-9]+)"
```

## Running a script

Scripts live in Opafra, not on the host. A step names one, and Opafra uploads it, runs
it, and removes it afterwards.

```yaml
- name: Run the migration check
  targets: [db-01]
  tool: ssh.script.file
  args:
    script_name: migration-precheck
    script_args: "--verbose"
```

Cleanup is the default, so the host is left as it was found. A script is stored per
tenant, so two organisations can each have a `deploy` script without collision.

Prefer a script over a very long `ssh.cmd` for anything past a few lines. A script is
reviewable, reusable across plans, and its body appears in the dry-run preview so an
approver can read what would run.

## Privilege

A command runs as the user the server was registered with. Where it needs more, use
`sudo` in the command, and remember that **`sudo` must not prompt**: a run cannot answer
a password prompt and will hang until it times out rather than succeeding.

> **Warning**
>
> `ssh.cmd` and `ssh.script.file` run whatever text they are given, with whatever privilege
> the connecting user holds. A mistake in a variable becomes a mistake on the host. These
> are the two tools most worth dry running, even though the preview can only show you the
> resolved text rather than predict its effect.

## Secrets in a command

Reference a secret rather than pasting one:

```yaml
command: "mysql -u root -p'secret://db/root_password' -e 'FLUSH LOGS'"
```

The reference is resolved at the moment of use. It is redacted in the dry-run preview, in
the run log, and in the execution record, so the plan stays safe to read and safe to keep
in git.

> **Danger**
>
> A literal password in a command is written into the plan, into the run log, and into
> every preview of it. Nothing can redact a value it was never told was secret.

## What a raw command still gives you

It is worth being clear that dropping to a command does not drop out of everything.
A raw step is still targeted per host, still subject to the
[approval gate](/docs/running/approval-gates) when the environment demands it, still
recorded per host in the run record, and still halts the run on failure.

What you lose is prediction and change reporting, not custody.

## Next steps

- [Tool catalogue](/docs/reference/tools) to check whether a tool already covers it
- [Dry runs](/docs/running/dry-run) for what a preview of a raw command can show
