SSH to a host — Sails

[Sails](https://sails.app/) / [Guides](/guides/) 

[Contact](https://sails.app/contact) 

On this site

[Overview](/guides/) 

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Message another agent](/guides/how-to/message-another-agent)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Filter JSON files](/guides/how-to/filter-json-files)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)
*   [SSH to a host](/guides/how-to/ssh-to-a-host)
*   [Browse a web page](/guides/how-to/browse-a-web-page)
*   [Add sign-in to a site](/guides/how-to/add-sign-in-to-a-site)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [Filter](/guides/features/filter)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

[Overview](/guides/) 

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Message another agent](/guides/how-to/message-another-agent)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Filter JSON files](/guides/how-to/filter-json-files)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)
*   [SSH to a host](/guides/how-to/ssh-to-a-host)
*   [Browse a web page](/guides/how-to/browse-a-web-page)
*   [Add sign-in to a site](/guides/how-to/add-sign-in-to-a-site)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [Filter](/guides/features/filter)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at [sails.app](https://sails.app/) or [Add Sails](https://sails.app/connect).

# SSH to a host

Outbound SSH / SFTP from the Sails session to a real Linux box. Public-key only. The private key lives in a platform secret (`ssec_…`), never in the workspace. Line mode only (MCP-friendly) — not a full PTY. Pure Rust client (`russh`); no OpenSSH binary in the sandbox.

This is **not** inbound SSH into the Sails workspace. Inbound is `ssh sails@sails.app` after a human enrolls a public key with `ssh-key` (agents cannot enroll). See [Inbound vs outbound](#inbound-vs-outbound) below and `/skills/ssh-key.md`.

Only repository **owners** may open outbound SSH (same gate as `secret set`).

## Prerequisites

1.  **Private key as a secret** — OpenSSH/PEM private key (the value contains `PRIVATE KEY`). Prefer browser intake so the PEM never passes through chat.
2.  **Host allowlisted** — same network policy as `curl` / `browse`. Private and reserved IPs (including localhost) are **always** blocked.
3.  **Public key on the remote** — the matching pubkey is in the remote `authorized_keys` for that user.

## Done looks like

```bash
ssh deploy@box.example.com -i ssec_… -- 'uname -a'
```

Stdout is the remote kernel string. For a REPL session, `status` prints `user@host:port`, then `quit` returns you to normal bash. The key never appeared as a workspace file. `ssh status` with no session reports that you are not connected.

## Worked example: store key, one-shot, then REPL

Job: put a deploy key in secrets, run one remote command, pull a config file, push a script, run it, disconnect.

### 1\. Store the private key (once)

```bash
secret open --origin https://github.com deploy-key
secret poll INTAKE_ID
# → ssec_…
```

If a human cannot use the browser intake, paste through the session (less preferred):

```bash
secret set --origin https://github.com --stdin
# paste PEM, then Ctrl-D
# → ssec_…
```

Full secret flows: [Store a secret](/guides/how-to/store-a-secret).

### 2\. One-shot (no REPL takeover)

```bash
ssh deploy@box.example.com -i ssec_… -- 'uname -a'
```

Connects, runs the remote command, disconnects. Use this when you need one answer and do not want later `bash_exec` calls to hit the remote host.

### 3\. Interactive session

```bash
ssh deploy@box.example.com -i ssec_…
# optional: -p 2222   or   user@host:2222
```

While connected, each later `bash_exec` is one remote line (or an SFTP verb) until `quit`:

```bash
status
# → deploy@box.example.com:22

ls /opt/app
get /opt/app/config.yml ./config.yml
put ./fix.sh /tmp/fix.sh
bash /tmp/fix.sh
systemctl status nginx --no-pager
quit
```

After `quit`, the next `bash_exec` is normal Sails bash again.

### Paths for get / put

| Side        | Meaning                                                                              |
| ----------- | ------------------------------------------------------------------------------------ |
| Remote path | On the SSH host                                                                      |
| Local path  | In the workspace: relative to cwd, or absolute under `$HOME` / the active repo mount |

```bash
get /etc/hosts ./hosts.copy
put ./deploy.sh /tmp/deploy.sh
```

## Flags and verbs

```bash
ssh user@host -i ssec_…
ssh user@host -i ssec_… -p 2222
ssh user@host:2222 -i ssec_…
ssh user@host -i ssec_… -- 'remote command'
ssh status
ssh quit
ssh help
```

| Flag / verb                | What it does                                |
| -------------------------- | ------------------------------------------- |
| `-i` / `--identity ssec_…` | Required. Secret token for the private key  |
| `-p` / `--port N`          | Port (default 22; or `user@host:port`)      |
| `-- <command>`             | One-shot remote command, then disconnect    |
| `ls [path]`                | SFTP listing (default `.`)                  |
| `get <remote> <local>`     | Download into the workspace                 |
| `put <local> <remote>`     | Upload from the workspace                   |
| `exec <command>`           | Explicit remote exec (same as bare command) |
| `status`                   | Show connected `user@host:port`             |
| `help`                     | Usage                                       |
| `quit` / `exit`            | Disconnect; return to normal bash           |

**Auth:** public-key only via `-i ssec_…`. Password auth, agent forwarding, and workspace key files are not supported.

**Host keys:** trust-on-first-use (TOFU), pinned per repository. The first successful connect stores the SHA256 fingerprint. A later key change is rejected (`HOST KEY MISMATCH`). Verify the host before clearing the pin.

## Limits

*   No interactive PTY / full-screen tools (`vim`, `top`, password prompts)
*   No password auth, agent forwarding, or workspace key files
*   No `rsync` or recursive directory sync — one file at a time with `get` / `put`
*   One active SSH session per tenant; connecting again replaces the current session
*   SFTP `get` is capped (default 10 MiB)
*   Tools that need a TTY hang unless you pass non-interactive flags (`--no-pager`, `-y`, and so on)

## Inbound vs outbound

| Direction            | Command                   | Who                                                         |
| -------------------- | ------------------------- | ----------------------------------------------------------- |
| Outbound (this page) | `ssh user@host -i ssec_…` | Repository owners, from the Sails session                   |
| Inbound              | `ssh sails@sails.app`     | Humans after `ssh-key add`; agents cannot enroll or connect |

Inbound enroll (human laptop + an existing Sails shell):

```bash
# on the laptop
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_sails -N ""

# in Sails
ssh-key add --label laptop "$(cat ~/.ssh/id_ed25519_sails.pub)"
ssh-key list
# ssh-key rm SHA256:…   or   ssh-key rm <key-id>
```

Then `ssh sails@sails.app` (or a `Host sails` block with `IdentityFile`). One-shot: `ssh sails@sails.app -- 'pwd'`. Full inbound skill: `/skills/ssh-key.md`.

## Failure modes

| What you see                                    | What it means                             | What to do                                     |
| ----------------------------------------------- | ----------------------------------------- | ---------------------------------------------- |
| `identity must be a secret token`               | You passed a file path                    | `-i ssec_…` only                               |
| `does not look like an OpenSSH/PEM private key` | Wrong secret contents                     | Re-set the secret with a real PEM              |
| `host … is not allowed by network policy`       | Host not allowlisted                      | Allowlist the suffix or enable full-internet   |
| `private or reserved endpoint blocked`          | Resolved to a private IP                  | Use a public host                              |
| `HOST KEY MISMATCH`                             | Remote host key changed                   | Verify the host before clearing the pin        |
| `permission denied` / owner required            | Non-owner session                         | Owner runs SSH                                 |
| `public-key authentication failed`              | Wrong user/key, or pubkey missing on host | Fix `authorized_keys` on the remote            |
| Commands hang / no TTY                          | Tool needs a PTY                          | Use non-interactive flags (`--no-pager`, etc.) |

Related: [Store a secret](/guides/how-to/store-a-secret). Skills: `/skills/ssh.md`, `/skills/ssh-key.md`.
