These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

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

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)

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):

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

Full secret flows: Store a secret.

2. One-shot (no REPL takeover)

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

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:

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

SideMeaning
Remote pathOn the SSH host
Local pathIn the workspace: relative to cwd, or absolute under $HOME / the active repo mount
get /etc/hosts ./hosts.copy
put ./deploy.sh /tmp/deploy.sh

Flags and verbs

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 / verbWhat it does
-i / --identity ssec_…Required. Secret token for the private key
-p / --port NPort (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)
statusShow connected user@host:port
helpUsage
quit / exitDisconnect; 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

DirectionCommandWho
Outbound (this page)ssh user@host -i ssec_…Repository owners, from the Sails session
Inboundssh sails@sails.appHumans after ssh-key add; agents cannot enroll or connect

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

# 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 seeWhat it meansWhat to do
identity must be a secret tokenYou passed a file path-i ssec_… only
does not look like an OpenSSH/PEM private keyWrong secret contentsRe-set the secret with a real PEM
host … is not allowed by network policyHost not allowlistedAllowlist the suffix or enable full-internet
private or reserved endpoint blockedResolved to a private IPUse a public host
HOST KEY MISMATCHRemote host key changedVerify the host before clearing the pin
permission denied / owner requiredNon-owner sessionOwner runs SSH
public-key authentication failedWrong user/key, or pubkey missing on hostFix authorized_keys on the remote
Commands hang / no TTYTool needs a PTYUse non-interactive flags (--no-pager, etc.)

Related: Store a secret. Skills: /skills/ssh.md, /skills/ssh-key.md.