> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boat.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# SSH Access

> Connect to a sandbox over SSH from your terminal or external tools.

<video controls playsInline preload="metadata" poster="/videos/ssh-access.jpg" src="https://mintcdn.com/ariana-1e97e6c3/KkeC9MhSefNUGJb6/videos/ssh-access.mp4?fit=max&auto=format&n=KkeC9MhSefNUGJb6&q=85&s=824cd066f72378c49be1450fc19d6684" className="w-full aspect-video rounded-xl" data-path="videos/ssh-access.mp4" />

Sandboxes accept standard SSH connections. The Boat CLI handles the usual setup for you, including the SSH key it uses to connect.

## Connect with the CLI

Use `boat ssh` from your local machine for an interactive shell. For non-interactive commands, use the same sandbox work access through the SDK, API, curl, or CLI:

<CodeGroup>
  ```bash CLI theme={null}
  boat ssh bx_f7k2q9hd
  boat ssh bx_f7k2q9hd "cd /home/user/my-repo && npm test"
  boat ssh bx_f7k2q9hd -- bash -lc "cd /home/user/my-repo && npm test"
  boat ssh bx_f7k2q9hd -- bash -s < ./setup.sh
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"command":"npm test","cwd":"my-repo","timeoutSeconds":60}'
  ```

  ```ts TypeScript theme={null}
  const result = await sandbox.command({
    sandboxId: "bx_f7k2q9hd",
    command: "npm test", cwd: "my-repo", timeoutSeconds: 60,
  });
  console.log(result.stdout);
  ```

  ```python Python theme={null}
  from boat_sdk.models.command_request import CommandRequest

  result = sandbox.command(
      "bx_f7k2q9hd",
      CommandRequest(command="npm test", cwd="my-repo", timeout_seconds=60),
  )
  print(result.stdout)
  ```
</CodeGroup>

The CLI manages its SSH key at:

```text theme={null}
~/.ssh/ascii_box_ed25519
```

If the key is missing, the CLI creates or refreshes it and authorizes it on the sandbox before connecting.

<Note>
  SSH is available only when the sandbox's machine is running. If the sandbox is archived or still provisioning, resume it or wait until it is ready. In the first seconds of a resume, SSH may answer with a retryable `sandbox_restoring` error; retry a moment later.
</Note>

## Use another SSH client

If you want to connect from an external SSH client, first authorize a public key and inspect the sandbox for its IP address:

<CodeGroup>
  ```bash CLI theme={null}
  boat info bx_f7k2q9hd
  ssh -i ~/.ssh/ascii_box_ed25519 user@<sandbox-ip>
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/sshkey" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"key":"ssh-ed25519 AAAAC3Nza... you@example.com"}'

  curl -sS "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd" \
    -H "Authorization: Bearer $BOAT_API_KEY"
  ```

  ```ts TypeScript theme={null}
  await sandbox.sshKey({
    sandboxId: "bx_f7k2q9hd",
    key: "ssh-ed25519 AAAAC3Nza... you@example.com",
  });

  const info = await sandbox.get({ sandboxId: "bx_f7k2q9hd" });
  console.log(info.sandbox.ip);
  ```

  ```python Python theme={null}
  from boat_sdk.models.ssh_key_request import SshKeyRequest

  sandbox.ssh_key("bx_f7k2q9hd", SshKeyRequest(key="ssh-ed25519 AAAAC3Nza... you@example.com"))
  info = sandbox.get("bx_f7k2q9hd")
  print(info.sandbox.ip)
  ```
</CodeGroup>

## Verify the host key

Every resume and every fork runs the sandbox on a new machine. Each new machine has a new SSH host key. With strict host key checking, the key you saved before a stop is refused after the resume.

`boat ssh` and `boat scp` handle this for you. They fetch the current host key through your authenticated Boat session, pin it, and connect with strict checking.

For another SSH client, get the key the same way. The SSH key call returns `hostKey`, the public host key of the machine that runs the sandbox now. Save it under a fixed alias, and fetch it again after each resume:

<CodeGroup>
  ```bash CLI theme={null}
  boat ssh bx_f7k2q9hd   # fetches and pins the current host key itself
  ```

  ```bash curl theme={null}
  resp="$(curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/sshkey" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"key\":\"$(cat ~/.ssh/id_ed25519.pub)\"}")"

  echo "bx_f7k2q9hd $(echo "$resp" | jq -r .hostKey)" > ~/.ssh/known_hosts_bx_f7k2q9hd

  # sshEndpoint ("host:port") is set when the machine has no public IPv4 of its own
  endpoint="$(echo "$resp" | jq -r '.sshEndpoint // empty')"
  host="${endpoint%:*}"; port="${endpoint##*:}"
  [ -n "$endpoint" ] || { host="$(echo "$resp" | jq -r .machineIp)"; port=22; }

  ssh -p "$port" \
      -o StrictHostKeyChecking=yes \
      -o UserKnownHostsFile=~/.ssh/known_hosts_bx_f7k2q9hd \
      -o HostKeyAlias=bx_f7k2q9hd \
      user@"$host"
  ```

  ```ts TypeScript theme={null}
  const { hostKey, machineIp, sshEndpoint } = await sandbox.sshKey({
    sandboxId: "bx_f7k2q9hd",
    key: "ssh-ed25519 AAAAC3Nza... you@example.com",
  });
  // write `bx_f7k2q9hd ${hostKey}` to a known_hosts file, then connect with HostKeyAlias=bx_f7k2q9hd
  // to sshEndpoint ("host:port") when it is set, else to machineIp on port 22
  ```

  ```python Python theme={null}
  from boat_sdk.models.ssh_key_request import SshKeyRequest

  resp = sandbox.ssh_key("bx_f7k2q9hd", SshKeyRequest(key="ssh-ed25519 AAAAC3Nza... you@example.com"))
  # write f"bx_f7k2q9hd {resp.host_key}" to a known_hosts file, then connect with HostKeyAlias=bx_f7k2q9hd
  # to resp.ssh_endpoint ("host:port") when it is set, else to resp.machine_ip on port 22
  ```
</CodeGroup>

The key comes over your API connection, not over SSH, so you can trust it without a first-connection prompt.

## Copy files

Use `boat scp` with the sandbox ID as the remote host, or use file/artifact API calls when you are building an integration:

<CodeGroup>
  ```bash CLI theme={null}
  boat scp ./local-file.txt bx_f7k2q9hd:/home/user/
  boat scp bx_f7k2q9hd:/home/user/output.zip ./output.zip
  ```

  ```bash curl theme={null}
  curl -sS -X PUT "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/files" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"path":"notes/input.txt","content":"hello\n","encoding":"utf8"}'

  curl -L -o output.zip "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/artifacts?path=output.zip" \
    -H "Authorization: Bearer $BOAT_API_KEY"
  ```

  ```ts TypeScript theme={null}
  await sandbox.writeFile({
    sandboxId: "bx_f7k2q9hd",
    path: "notes/input.txt", content: "hello\n", encoding: "utf8",
  });

  const artifact = await sandbox.artifact({ sandboxId: "bx_f7k2q9hd", path: "output.zip" });
  ```

  ```python Python theme={null}
  from boat_sdk.models.file_write_request import FileWriteRequest

  sandbox.write_file("bx_f7k2q9hd", FileWriteRequest(
      path="notes/input.txt",
      content="hello\n",
      encoding="utf8",
  ))
  artifact = sandbox.artifact("bx_f7k2q9hd", "output.zip")
  ```
</CodeGroup>

File read/write paths may be relative (resolved from the sandbox work directory, `/home/user`) or absolute; an absolute path must resolve under `/home/user` or `/tmp`, anything else is refused with a 400 `invalid_path` error naming the allowed roots.

See [CLI Reference](/cli-reference#boat-scp) for all `scp` options.

## Related

* [Environments](/environments)
* [Long-Running Tasks](/long-running-tasks)
* [Desktop Streaming](/desktop-streaming)
* [Hosting](/hosting)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.