# Run remote commands with SSH

import { Steps, TabItem, Tabs } from "@astrojs/starlight/components"

SSH (Beta) is a Mindflow integration that runs a **non-interactive** command on a remote host over SSH. Each step runs
one command, then returns exit code, stdout, and stderr. You authenticate with an **SSH Password** or **SSH Private
Key** credential; the host always comes from that credential.

## When to use

- You need to run a script, CLI, or health-check on a server you already reach over SSH.
- You chain several shell lines that must share the same directory or environment in **one** step.
- Prefer [Exchange files with SFTP](../exchange-files-with-sftp/) when the job is upload, download, list, or rename on
  the remote filesystem.
- Prefer a native integration when the remote tool exposes a documented REST or GraphQL API.

Before you begin, you need an SSH server you can reach on **TCP port 22** (or the port in your credential URL) and
permission to create a credential in the environment. Ask your Mindflow administrator for the platform **static egress
IP**, then allow that address inbound on the server firewall or security group. See
[Credentials](/getting-started/credentials/).

## Guide

### Find SSH

Enable **SSH (Beta)** in the current environment, then add **Run SSH command** from the step menu.

<ol>
<li>

**Open Integrations.** In Studio, go to **Integrations**.

</li>
<li>

**Open the Mindflow vendor group.** Search for `ssh`, then enable **SSH (Beta)** if it is not already on.

<div class="docs-screenshot-compact">

![Integrations · Mindflow shows SSH (Beta) 1.0.0 as a native integration after searching for ssh](./assets/find-ssh.png)

</div>

</li>
<li>

**Add the step.** In a flow, open **Add a step**, choose **Integrations**, then **SSH (Beta)** → **Run SSH command**.

<div class="docs-screenshot-compact">

![Add a step menu for SSH (Beta) showing Run SSH command](./assets/ssh-operation.png)

</div>

</li>
</ol>
### Create an SSH credential

Every SSH step authenticates with an **SSH Password** or **SSH Private Key** credential in the environment. Host and
port come from the credential **Address** URL (`ssh://` or `sftp://`), not from the step body.

<ol>
<li>

**Open Environments.** Open the environment that will run the flow, then choose **Add credential**.

</li>
<li>

**Choose SSH (Beta).** Set **Service** to **SSH (Beta)**. Set **Credential name**, and keep **URL** as **Custom**.

</li>
</ol>
Then choose the authentication flavor:

Use **SSH Password** when the server authenticates with a username and password. Fill **Address**, **User name**, and
**Password**.

<div class="docs-screenshot-compact">

![Credential form for SSH (Beta): SSH Password, address ssh://bastion.ops.example:22, user name mindflow-ops](./assets/ssh-password-credential.png)

</div>

Open **Advanced**, set **Credential type** to **SSH Private Key**, then fill **Address** and **User name**. Paste the
full PEM (including `BEGIN` and `END` lines) into **Private key**. Fill **Passphrase** only when the key is encrypted.

<div class="docs-screenshot-compact">

![Credential form for SSH (Beta): SSH Private Key, filled address, user name, and private key](./assets/ssh-private-key-credential.png)

</div>

### Run a command

Fill the step input with the remote command and optional shell context. The host always comes from the credential.

:::caution[Stateless execution]
Each **Run SSH command** step starts a **brand-new, isolated shell**. Nothing from a previous step — `export`
variables, `cd` directory changes, background processes, or other shell state — carries to the next step, even when
both steps reuse the same credential or SSH connection.

This mistake is **silent**: the later step can still succeed; it just lacks the expected prior state, so no error
surfaces.

| Anti-pattern (two steps)                     | Correct pattern                                                                     |
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
| Step 1: `cd /app` · Step 2: `ls`             | One step: `cd /app && ls`, or **Working directory** `/app` + command `ls`           |
| Step 1: `export FOO=1` · Step 2: uses `$FOO` | **Environment** `FOO=1` on the same step, or `export FOO=1 && …` in one **Command** |

Prefer **Working directory** over a prior `cd`, **Environment variables** over a prior `export`, and chain dependent
logic (`&&`, `;`, multi-line scripts) inside a **single** **Command**.
:::

<ol>
<li>

**Open Settings on the step.** Select the SSH credential.

<div class="docs-screenshot-compact">

![Run SSH command Settings: Credential set to Bastion ops SSH on Check bastion disk before batch](./assets/check-disk-credential.png)

</div>

</li>
<li>

**Open Input on the step.** Set **Command** to the non-interactive shell string to run. Absolute paths in the command
are fine; maximum length is **8192** characters.

<div class="docs-screenshot-compact">

![Run SSH command Input: Command df -h /, Fail on non-zero exit on, on Check free disk on bastion](./assets/check-disk-input.png)

</div>

</li>
<li>

**Optional: open Advanced.** Expand **Advanced** on the Input tab when you need **Working directory**, **Environment
variables**, or **Standard input**.

</li>
<li>

**Optional: set Working directory.** When the command must start in a fixed path, set **Working directory** instead of
a prior `cd` step. Relative paths resolve from the remote login directory. Maximum length is **1024** characters.

</li>
<li>

**Optional: set Environment variables.** Add key/value pairs the remote process should see. Prefer this over a prior
`export` step. Keys must match `^[A-Za-z_][A-Za-z0-9_]*$` (at most **128** characters). You can set up to **50**
variables; each value is at most **4096** characters.

</li>
<li>

**Optional: set Standard input.** Paste a UTF-8 string written once to the command stdin, then closed (EOF). Maximum
size is **65536** UTF-8 bytes. Leave it empty when the command does not read stdin.

</li>
<li>

**Choose Fail on non-zero exit.** Leave it on (default) so a failed remote command fails the Studio step while stdout
and stderr stay in the step result. Turn it off only when you want the flow to continue and branch on `exitCode`
yourself.

</li>
<li>

**Set step Timeout.** Open the step **Settings** and set **Timeout** to how long the command may run (default is
30 seconds; maximum is **840** seconds). SSH does not expose a separate timeout field in the request body. See
[Handle timeout](../handle-timeout/).

</li>
</ol>
### Read the result

After a successful connect, the step result includes `exitCode`, `stdout`, `stderr`, and whether the command timed out
(`timedOut`). Use those fields in later steps or conditions. When **Fail on non-zero exit** is on (default), a
non-zero exit, missing exit code, or timeout fails the step with the same result body.

1. **Inspect stdout.** Read the captured standard output from the step result when the remote tool prints status or
   JSON.
2. **Branch on exit code.** When **Fail on non-zero exit** is off, add a condition on `exitCode` (for example `0` for
   success).

## Example

The screenshots on this page use **Check bastion disk before batch**.

1. **Create the credential.** Add an **SSH Password** or **SSH Private Key** credential named **Bastion ops SSH** for
   `ssh://bastion.ops.example:22` as `mindflow-ops`.
2. **Add and rename the step.** Add **Run SSH command**, rename it to **Check free disk on bastion**, and describe why
   the batch depends on free disk.
3. **Configure and dry-run.** Set **Command** to `df -h /`, keep **Fail on non-zero exit** on, set step **Timeout** to
   60 seconds if needed, then dry-run. Confirm `exitCode` is `0` and `stdout` lists the root filesystem. A later
   condition can fail the run when stdout shows the root filesystem above a capacity threshold.

## Keep in mind

- **Beta:** SSH (Beta) may change; confirm operation labels in Studio if a release renames them.
- Each step is an isolated shell. Chain `cd`, `export`, and dependent commands in **one** **Command**, or use
  **Working directory** and **Environment variables**.
- Interactive shells, PTY sessions, and TCP forwarding are out of scope.
- For file transfer, use [Exchange files with SFTP](../exchange-files-with-sftp/) instead of piping files through SSH
  commands.
- Captured stdout and stderr are size-capped; oversized output fails the step.
- **Network:** The connector connects from the platform static egress IP. A connect timeout fails the step as a
  **gateway timeout** (HTTP **504**) and usually means the firewall or security group does not allow that IP.
- **Private keys:** Paste the full PEM, including line breaks. A key that cannot be parsed often lost those breaks in
  the form.

Related: [Reach out to services](../) · [Exchange files with SFTP](../exchange-files-with-sftp/) ·
[Handle timeout](../handle-timeout/) · [Credentials](/getting-started/credentials/)
