# Connect to a space

> Reach a moatbox space over the SSH gateway, from your editor through Open in…, with the moatbox CLI, or over Tailscale.

A moatbox space is a virtual machine on moatbox servers or on a server your team links. You reach it four ways: SSH through the moatbox gateway, an editor or agent app set up from the dashboard's **Open in…** menu, the `moatbox` CLI, or your own Tailscale tailnet. All of them start from an account at `https://app.moatbox.dev` and membership in the space's team. For the space itself, see [Spaces](https://docs.moatbox.dev/spaces.md).

## SSH keys

The gateway knows you by your SSH public key. Add keys under **Settings → General → SSH keys**: paste one line from a file such as `~/.ssh/id_ed25519.pub`. One key belongs to one moatbox account; adding a key that another account holds fails with `409`. **Revoke** removes a key, and moatbox closes your open sessions within about 15 seconds.

The CLI setup command adds a key for you; see [Set up the CLI](#set-up-the-cli).

## The SSH gateway

Each space answers on the gateway under the SSH user `<space>`. moatbox gives it the alias `moatbox-<space>`. The block for `~/.ssh/config` looks like this:

```sshconfig
Host moatbox-my-space
  HostName <gateway host>
  Port <gateway port>
  User my-space
  IdentityFile "~/.ssh/id_ed25519"
  IdentitiesOnly yes
```

Don't type it by hand. Get the exact block, with the gateway's host key for `~/.ssh/known_hosts`, in one of these ways:

- `moatbox connect my-space --for ssh --write` writes both files for you.
- **Open in…** → **Terminal** → **Rather not install anything? Paste the SSH config by hand** shows both.
- `GET /api/v1/spaces/my-space/ssh` returns them as JSON (`config`, `known_hosts`, `alias`, `host`, `port`, `user`).

**Copy SSH** on the space's Overview page copies a plain `ssh` command instead.

Then:

```sh
ssh moatbox-my-space
scp ./notes.txt moatbox-my-space:/workspace/
rsync -av moatbox-my-space:/workspace/out/ ./out/
```

### What the gateway relays

- interactive shells, one-off commands, `sftp`, and so `scp` and `rsync`
- local port forwards (`ssh -L`)

The gateway refuses other channel types and declines remote forward requests, so `ssh -R` does not work.

### Where you land

The space's workspace mode decides the user:

| Mode | SSH user inside the space | Notes |
|------|---------------------------|-------|
| Full machine | `dev` | In the VM, with passwordless sudo and Docker |
| Locked | `agent` | In the gVisor sandbox container, no root |

Teams on the Team plan start locked; other plans start on full machine. Switch with **Full machine** on the space's Agents page or with `moatbox space mode`. On the Team plan, only owners and admins can turn full machine on. A team owner can forbid full machine. Switching closes open SSH sessions. Once someone has opened an SSH session in full machine mode, going back to locked takes a reset.

### What happens on connect

- Connecting to a sleeping space wakes it. A space that is read-only, paused by moatbox or still starting refuses the session.
- moatbox records each session on the space's timeline: who opened it, the command line of a one-off command, bytes in and out, and the exit code. Shell output goes on the timeline too, unless monitoring is off for the space (see [Privacy](https://docs.moatbox.dev/privacy.md)). Interactive sessions also emit the `run.started` [event](https://docs.moatbox.dev/events-and-webhooks.md).
- Traffic on a session counts as activity and keeps the space awake. See [Auto-stop](https://docs.moatbox.dev/spaces.md#auto-stop).
- Every 15 seconds the gateway checks that your key, your team membership and the workspace mode still hold. If one changed, or the space went to sleep, it closes the connection.

## Open in…

**Open in…** sits on the space's **Overview** page and under the space picker. It opens a side panel for the tool you pick. The menu lists editors and agent apps: VS Code, Cursor, Claude Code, Codex, opencode, T3 Code and a plain **Terminal**.

For tools that use SSH, the panel starts with **Set up this computer once**: a one-time command that installs the CLI and writes the SSH config (next section). Then it shows the tool's steps:

- **VS Code**: **Open in VS Code** opens `vscode://vscode-remote/ssh-remote+moatbox-<space>/workspace`. It needs the Remote - SSH extension. **Cursor** works the same way with a `cursor://` link.
- **Claude Code**: `ssh moatbox-<space>` then `claude`, and log in once from the URL it prints. Remote control needs a Claude subscription login. Run `ssh -t moatbox-<space> claude remote-control` by hand once to answer its questions, then keep it running with `moatbox connect <space> --for claude --remote-control`.
- **Codex**: turn on device code login in your ChatGPT settings, then `ssh moatbox-<space>`, `codex login --device-auth` and `codex`.
- **Terminal**: `ssh`, `scp` and `rsync` with the alias.

The opencode and T3 Code entries start a server in the space from the panel. It listens on your tailnet, so the space needs to join it first; see [Tailscale](#tailscale). For agent credentials in general, see [Agent sign-in](https://docs.moatbox.dev/agent-sign-in.md).

## Set up the CLI

The **Set up this computer once** command looks like this:

```sh
curl -fsSL https://app.moatbox.dev/api/v1/cli.sh | sh -s -- mbx_cli_… my-space
```

It:

1. downloads the `moatbox` binary for macOS (arm64, x86-64) or Linux (x86-64, arm64), checks its SHA-256, and installs it in `~/.local/bin` (set `MOATBOX_BIN_DIR` to change that)
2. creates `~/.ssh/moatbox_ed25519` if it does not exist
3. runs `moatbox login` with the code, naming this computer after the computer's name (set `MOATBOX_LABEL` to change that)
4. runs `moatbox connect my-space --for ssh --write`

The code works once and expires after 15 minutes. Press **New command** for another.

### moatbox login

```sh
moatbox login --code mbx_cli_… [--api https://app.moatbox.dev] [--label my-laptop] [--ssh-key ~/.ssh/moatbox_ed25519.pub]
```

`login` trades the one-time code for an API key named "moatbox CLI on <label>" and adds the public key to your account. The key lasts 365 days and shows under **Settings → API keys**. The CLI saves it in `~/.config/moatbox/credentials.json` with mode 600 (under `$XDG_CONFIG_HOME` when set).

For a pipeline, pipe a team API key in instead:

```sh
printenv MOATBOX_API_KEY | moatbox login --key-stdin
```

The key must start with `mbx_`. See [API keys](https://docs.moatbox.dev/api-keys.md) and [CI](https://docs.moatbox.dev/ci.md).

### Settings the CLI reads

| Variable | Use |
|----------|-----|
| `MOATBOX_API` | API base URL. Default: the one `login` saved, when the CLI uses the saved key; else `https://app.moatbox.dev` |
| `MOATBOX_API_KEY` | A team API key. Default: the key `login` saved |
| `MOATBOX_TEAM` | Team ID for `moatbox new`, when the key reaches several teams |

## CLI commands

These commands talk to the hosted API. Run `moatbox` with no arguments for the full usage.

| Command | What it does |
|---------|--------------|
| `moatbox login --code <code>` / `--key-stdin` | Sign this computer in |
| `moatbox connect <space> --for ssh\|claude\|codex [--write] [--remote-control] [-i <key>]` | Print the SSH config block, or write it with `--write`; prints the tool's login steps |
| `moatbox cp [-agent claude] <src> <dst>` | Copy files to, from or between spaces |
| `moatbox exec [--cwd <dir>] [--env NAME=value]... [--timeout 1h] <space> -- <command> [args...]` | Run a command in a space, stream its output, exit with its exit code |
| `moatbox new [--from <snapshot>] [--size small\|medium\|large\|xlarge] [--ttl 30m] [--name <space>] [--wait 10m]` | Create a space, wait until it is up, print its name |
| `moatbox delete --yes <space>` | Move a space to the trash |
| `moatbox reset [--keep-logins] [--undo] <space>` | Snapshot a space and re-create its disk from the current base image |
| `moatbox net <space>`, `net allow\|deny <space> <host>...`, `net mode <space> open\|restricted` | Show or change the space's network allowlist |
| `moatbox space mode <space> [full\|locked]` | Show or switch the workspace mode |
| `moatbox github connect <space>` | Hand the GitHub CLI's token on this computer to the team's GitHub integration; see [Integrations](https://docs.moatbox.dev/integrations.md) |
| `moatbox ci ...` | List CI jobs and logs; see [CI](https://docs.moatbox.dev/ci.md) |
| `moatbox version` | Print the CLI version |

Details:

- **connect** looks for a key in `~/.ssh` (`moatbox_ed25519`, then `id_ed25519`, `id_ecdsa`, `id_rsa`) unless you pass `-i`. `--write` replaces only the `Host moatbox-<space>` block in `~/.ssh/config`, makes no backup, and warns when an earlier line in the file overrides the block. `--remote-control` goes with `--for claude` and runs `claude remote-control` in the space while the connection stays open.
- **cp** takes a local path or `<space>:<path>` on each side. The space path sits under `/workspace` or `/home/agent`; a relative path means `/workspace/<path>`, and a trailing `/` keeps the source's name. Folders travel as tar, and `cp` skips symlinks inside a folder it uploads. Space-to-space copies go through moatbox. Transfers stop at 2 GiB.
- **new** defaults to `--size medium`, a name of `run-` plus random letters, and `--wait 10m`. `--ttl` deletes the space after that long, up to 30 days.
- **new** needs an API key with `space.create`. **delete** and `--ttl` need `space.delete`, or `space.delete-own` for a space that key created. The `ci` preset has `space.create` and `space.delete-own`. The key from `moatbox login --code` cannot create or delete spaces.

The same binary has more subcommands (`run`, `pending`, `approve`, `deny`, `undo`, `log`, `timeline`, `serve`, `deploy` and others). They work against a local moatbox daemon and its store, not the hosted API.

## Tailscale

A space can join your Tailscale tailnet as a device named after the space. Use it to reach servers the space runs from your own devices.

Owners and admins set it up on the space's **Network** page, in the **Tailscale** card:

1. In the Tailscale admin console, add `tag:moatbox` to `tagOwners` in the access controls.
2. Under **Settings → Keys**, generate an auth key: **Reusable** off, **Ephemeral** off, **Pre-approved** on, tagged `tag:moatbox`. Tailscale removes an ephemeral device while the space sleeps.
3. Paste the key (`tskey-…`) and press **Join tailnet**.

moatbox stores the key encrypted and never shows it again. **Remove** takes the space off the tailnet. Both actions wake a sleeping space. Over the API: `PUT /api/v1/spaces/<space>/tailscale` with `{"auth_key": "tskey-…"}`, `GET` to read the state, `DELETE` to leave.
